使用类的__new__方法作为工厂:__init__被调用两次
我在使用Python时遇到了一个奇怪的bug,使用一个类的__new__
方法作为工厂时,会导致这个类的__init__
方法被调用两次。
最开始的想法是利用母类的__new__
方法,根据传入的参数返回她的某个子类的特定实例,这样就不需要在类外面再声明一个工厂函数。
我知道使用工厂函数是这里最好的设计模式,但在项目的这个阶段更改设计模式会很麻烦。因此,我想问:有没有办法避免__init__
被调用两次,只让它被调用一次呢?
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
任何帮助都非常感谢。
3 个回答
我其实在我安装的两个Python解释器中都无法重现这个行为,所以这只是我的猜测。不过……
__init__
被调用了两次,是因为你在初始化两个对象:一个是原始的Shape
对象,另一个是它的一个子类。如果你修改你的__init__
方法,让它也打印出正在初始化的对象的类名,你就会看到这一点。
print type(self), "init called"
这并没有什么坏处,因为原始的Shape
对象会被丢弃,因为你在__new__()
中没有返回对它的引用。
由于调用一个函数在语法上和实例化一个类是一样的,你可以把这个改成一个函数,而不需要改变其他任何东西,我建议你就这么做。我不明白你为什么不愿意这样做。
在我发问之后,我继续寻找解决办法,发现了一种看起来有点像“黑科技”的解决方法。虽然这个方法不如Duncan的方案好,但我觉得提到它也挺有意思的。Shape
类变成了:
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
当你在Python中创建一个对象时,Python会先调用这个对象的 __new__
方法来生成对象,然后再调用 __init__
方法来初始化这个对象。当你在 __new__
方法里通过调用 Triangle()
来创建对象时,这会导致 __new__
和 __init__
方法被多次调用。
你应该这样做:
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)
这样可以创建一个 Rectangle
或 Triangle
,而不会触发 __init__
方法的调用,之后 __init__
方法只会被调用一次。
接下来回答 @Adrian 的问题,关于 super
是怎么工作的:
super(Shape, cls)
会在 cls.__mro__
中查找 Shape
,然后继续向下查找剩下的顺序来找到所需的属性。
Triangle.__mro__
是 (Triangle, Shape, object)
,而 Rectangle.__mro__
是 (Rectangle, Shape, object)
,Shape.__mro__
则是 (Shape, object)
。在这些情况下,当你调用 super(Shape, cls)
时,它会忽略 Shape
之前的所有内容,所以剩下的只有单个元素的元组 (object,)
,这个元组用来查找所需的属性。
如果你有菱形继承,这个情况会变得更复杂:
class A(object): pass
class B(A): pass
class C(A): pass
class D(B,C): pass
在这种情况下,B 类中的一个方法可能会使用 super(B, cls)
,如果它是 B 的实例,它会查找 (A, object)
,但如果你有一个 D 的实例,那么在 B 中的同样调用会查找 (C, A, object)
,因为 D.__mro__
是 (B, C, A, object)
。
所以在这种特定情况下,你可以定义一个新的混合类,来修改形状的构造行为,你可以让专门的三角形和矩形从现有的类继承,但构造方式不同。