Using a class' __new__ method as a Factory: __init__ gets called twice
Solution 1
When you construct an object Python calls its __new__
method to create the object then calls __init__
on the object that is returned. When you create the object from inside __new__
by calling Triangle()
that will result in further calls to __new__
and __init__
.
What you should do is:
class Shape(object):
def __new__(cls, desc):
if cls is Shape:
if desc == 'big': return super(Shape, cls).__new__(Rectangle)
if desc == 'small': return super(Shape, cls).__new__(Triangle)
else:
return super(Shape, cls).__new__(cls, desc)
which will create a Rectangle
or Triangle
without triggering a call to __init__
and then __init__
is called only once.
Edit to answer @Adrian's question about how super works:
super(Shape,cls)
searches cls.__mro__
to find Shape
and then searches down the remainder of the sequence to find the attribute.
Triangle.__mro__
is (Triangle, Shape, object)
and
Rectangle.__mro__
is (Rectangle, Shape, object)
while Shape.__mro__
is just (Shape, object)
.
For any of those cases when you call super(Shape, cls)
it ignores everything in the mro squence up to and including Shape
so the only thing left is the single element tuple (object,)
and that is used to find the desired attribute.
This would get more complicated if you had a diamond inheritance:
class A(object): pass
class B(A): pass
class C(A): pass
class D(B,C): pass
now a method in B might use super(B, cls)
and if it were a B instance would search (A, object)
but if you had a D
instance the same call in B
would search (C, A, object)
because the D.__mro__
is (B, C, A, object)
.
So in this particular case you could define a new mixin class that modifies the construction behaviour of the shapes and you could have specialised triangles and rectangles inheriting from the existing ones but constructed differently.
Solution 2
After posting my question, I continued searching for a solution an found a way to solve the problem that looks like a bit of a hack. It is inferior to Duncan's solution, but I thought it could be interesting to mention none the less. The Shape
class becomes:
class ShapeFactory(type):
def __call__(cls, desc):
if cls is Shape:
if desc == 'big': return Rectangle(desc)
if desc == 'small': return Triangle(desc)
return type.__call__(cls, desc)
class Shape(object):
__metaclass__ = ShapeFactory
def __init__(self, desc):
print "init called"
self.desc = desc
![xApple](https://i.stack.imgur.com/Ihzmb.png?s=256&g=1)
xApple
Updated on June 04, 2022Comments
-
xApple about 2 years
I encountered a strange bug in python where using the
__new__
method of a class as a factory would lead to the__init__
method of the instantiated class to be called twice.The idea was originally to use the
__new__
method of the mother class to return a specific instance of one of her children depending on the parameters that are passed, without having to declare a factory function outside of the class.I know that using a factory function would be the best design-pattern to use here, but changing the design pattern at this point of the project would be costly. My question hence is: is there a way to avoid the double call to
__init__
and get only a single call to__init__
in such a schema ?class Shape(object): def __new__(cls, desc): if cls is Shape: if desc == 'big': return Rectangle(desc) if desc == 'small': return Triangle(desc) else: return super(Shape, cls).__new__(cls, desc) def __init__(self, desc): print "init called" self.desc = desc class Triangle(Shape): @property def number_of_edges(self): return 3 class Rectangle(Shape): @property def number_of_edges(self): return 4 instance = Shape('small') print instance.number_of_edges >>> init called >>> init called >>> 3
Any help greatly appreciated.
-
ncoghlan about 13 yearsThis is not correct - the first
__init__
call happens inside the outer__new__
call (whenTriangle()
andRectangle()
are called), but then, because an instance ofShape
is returned by__new__
, the originalShape()
call invokes__init__
again on that already initialised object. Note that if the object returned by__new__()
is not an instance ofShape()
then__init__
won't be called (which can foul up attempts to observe this behaviour if the class hierarchy isn't right). -
xApple about 13 yearsIndeed, both are syntactically identical. My reluctance stems from the fact that if I define a function called "Shape", I must rename my class to something like "_Shape". This will cause some variable renaming, of course, but mostly it will have complicated consequences on other things like the documentation that is generated by sphinx-autodoc.
-
kindall about 13 yearsI suppose you do want to expose documentation on those classes so you can't just declare them in the function. You might try just reassigning the instance's
__class__
attribute in__init__()
and not messing with__new__()
at all. -
kindall about 13 years... one of the pitfalls of this, by the way, is that you will have to manually re-bind the instance methods from the correct class to the instance.
-
kindall about 13 yearsAnother approach might be to use a metaclass to override the
__call__()
method on the class, so that the()
syntax does not necessarily instantiate the class. -
kindall about 13 years... in fact, the metaclass approach does work, but the solution you accepted is much simpler.
-
Stefan about 13 yearsWouldn't it be better to use
return Rectangle.__new__(Rectangle)
because this would guarantee that__new__
ofRectangle
gets called if it's defined? -
Duncan about 13 years@Georg, If you do that you are going to have to be pretty careful to avoid infinite recursion. Any class specific initialisation should be in
__init__
so I think it is pretty safe here to assume that__new__
's only job is to create an object of the correct type. -
martineau over 11 yearsThe code in your answer fixes the problem but doesn't explain why
Shape.__init__()
was called twice in the OP's code, even whenShape.__new__()
returns aTriangle(desc)
. This seems contrary to what the docs say: "If__new__()
does not return an instance ofcls
, then the new instance’s__init__()
method will not be invoked." -
martineau over 11 yearsNever mind, I'm guessing in the OP's code it's
isinstance(<returned object>, Shape)
beingTrue
for aTriangle
object that's allowing theShape.__init__()
method to be called on it. -
javawizard about 11 years@martineau: Precisely,
__init__
is called if the returned object is an instance of the specified class or a subclass. (It's my frank opinion that this is a Bad Thing, but alas, it's the way it is.) -
Andy Hayden almost 11 yearsWhy do you say this is inferior to Duncan's solution. This seems much clearer to what's going on, imo less hacky. Also meta.
-
xApple almost 11 yearsI thought it was less obvious because it adds a fourth class to the program and involves the
__metaclass__
black magic that not everyone is familiar with. -
Bob almost 8 years@GeorgSchölly
super(Shape, cls)
- in general, how doessuper
use the first and second arguments to return the thing it returns? I knowcls
must be a subclass ofShape
but how does the thing thatsuper
returns relate toShape
andcls
? -
Stefan almost 8 years@Adrian: I don't know all the details of super by heart, but you can have a look at the documentation or the code. It must be something like this:
super(Class, instance).method(args)
is the same asClass.method(instance, args)
whereinstance
is usually calledself
inside the method. -
Duncan almost 8 years@Adrian, I added a description of how
super
works. -
martineau almost 6 yearsAlthough most folks probably don't care, one advantage of using inheritance instead of a metaclass is that the syntax for specifying metaclasses is different in Python 3 that it is in Python 2—on the other hand, inheritance is written similarly in both of them. That fact could make it a better fit when attempting to write code that will work unchanged in both versions of Python.
-
geekscrap almost 4 yearsWhat would be the equivalent in python 3?
-
Marine Galantin about 3 yearsIMHO having metaclass is cleaner because here, one is trying to implemented a top down inheritence (creating child from parents) instead of the normal inheritance order down-top (creating parents from child). Metaclass seems to be the right way to do it :) Also, the whole purpose of
call
andnew
is to deal with such cases...