Today, we are going to confuse a constructor.
What is the constructor?
One of the first concepts encountered in object-oriented programming is that of the constructor, the method that is run immediately after an object is instantiated that configures and initializes the object.
In Python, a constructor is defined by the
The constructor is not permitted to return a value, because
constructing a new instance of class A should result in an object
of type A. Returning something would just be confusing things.
But does it ever make sense for a constructor of class A to return an object of type B? And if it does make sense, how do we go about doing it?
Rewiring the constructor to do... weird stuff
The answer lies in Python's
__new__ method, which is a method called
when a class is defined (not instantiated). The
__new__ method is
different from the
__init__ method, and does not do the same thing.
__new__ method for class A should only return the type of class A.
__new__ returns anything else, Python will not run the
method for class A.
For example, suppose we want a wrapper class that transparently constructs different kinds of objects conditionally - based on a configuration file, or the state of a file, or some other condition. We want to construct an object of type A and get back an object of type B, C, or D. How to do that?
First, let's look at how the
__new__ method works.
A simple example class
Start with a simple example class:
class A(object): def __init__(self, *args, **kwargs): print("Instance of class A created") def hello(self): print("Hello world")
Executing this gives:
In : my_object = A() Instance of class A created In : my_object.hello() Hello world
Now let's look at a class A where we define the
__new__ method. This method
controls how the instantiation of objects of type A work, so we can do something
like limiting the creation of objects of type A to when a certain condition is
import random def tossCoin(): if random.random() < 0.5: return True else: return False class A5050(object): def __new__(cls, *args, **kwargs): if not tossCoin(): raise RuntimeError("Count not create instance") instance = super(A5050, cls).__new__(cls, *args, **kwargs) return instance def __init__(self, *args, **kwargs): print("Instance of class A5050 created") def hello(self): print("Hello world")
Now we can run this block of code:
def make_a5050(): try: my_object = A5050() my_object.hello() except RuntimeError: print("Better luck next time!")
It takes a few tries:
In : make_a5050() Better luck next time! In : make_a5050() Instance of class A5050 created Hello world
__new__ method for the
A5050 class raises a runtime error
with a 50% probability. Otherwise, it calls the
of the parent class (
object, which returns a class of type
We pass the same arguments and keyword arguments (args/kwargs) on to the
__new__, but we could optionally modify them here (say, add
a keyword, or check the state of a file, or etc.).
This is just an example of how the instantiation behavior of a class
can be modified before its constructor is even called by using the
__new__ returns objects, not classes
In the above example, our
__new__ method returned the result of
a call to
__new__ of a parent class. What happens if
returns something else?
First, repeating an important point made above: if
__new__ for a class
returns anything other than that class type, then
__init__ will not
be called for that class.
That means that the
__new__ method should either return a class (if
returning the type of its parent class, like a normal
does), or it should return an instantiated object.
Let's imagine that we want to create different instances of different classes based on a command line flag passed to the script:
class BaseClass(object): def hello(self): print("Hello world from class %s"%(self.__class__.__name__)) class B(BaseClass): pass class C(BaseClass): pass class D(BaseClass): pass class A(object): def __new__(cls, args): if args.B: return B() elif args.C: return C() elif args.D: return D() else: raise RuntimeError("Could not create instance") if __name__=="__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument('-B', action='store_true', help='Return object of type B') parser.add_argument('-C', action='store_true', help='Return object of type C') parser.add_argument('-D', action='store_true', help='Return object of type D') args = parser.parse_args() a = A(args) print(type(a))
Now if we run this script and pass it different flags, we get
a with different types:
$ py wat2.py -h usage: wat2.py [-h] [-B] [-C] [-D] optional arguments: -h, --help show this help message and exit -B Return object of type B -C Return object of type C -D Return object of type D
Now try the three flags:
$ py wat2.py -B <class '__main__.B'> $ py wat2.py -C <class '__main__.C'> $ py wat2.py -D <class '__main__.D'>
Moving beyond argparse
The example above shows how the constructor can use
argparse options to determine what kind of object
to return with
__new__, but you can use other
types of conditions as well:
- using command line options (see argparse example above)
- using configuration file options
- using environment variable values
- checking status of a file or port
- checking whether internet connection is available
__new__ in your patterns
We have already covered the Registry
pattern in a prior blog post, but the
__new__ method lends itself well to
all kinds of other patterns, including the
Singleton pattern and the Factory pattern.
There are some very useful patterns covered in this Github repository: https://github.com/faif/python-patterns