使用类'__new__方法作为工厂:__ init__被调用两次

时间:2011-05-10 17:11:00

标签: python design-patterns inheritance class-design

我在python中遇到一个奇怪的错误,其中使用类的__new__方法作为工厂会导致实例化类的__init__方法被调用两次。

这个想法最初是使用母类的__new__方法根据传递的参数返回她的一个孩子的特定实例,而不必在类之外声明工厂函数。

我知道使用工厂功能将是这里使用的最佳设计模式,但是在项目的这一点上改变设计模式将是昂贵的。我的问题是:有没有办法避免对__init__的双重调用,并且在这种模式中只能调用__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 个答案:

答案 0 :(得分:53)

构造对象时,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)

将创建RectangleTriangle而不会触发对__init__的调用,然后__init__只会被调用一次。

编辑回答@Adrian关于超级工作原理的问题:

super(Shape,cls)搜索cls.__mro__以查找Shape,然后搜索序列的其余部分以查找属性。

Triangle.__mro__(Triangle, Shape, object)Rectangle.__mro__(Rectangle, Shape, object),而Shape.__mro__仅为(Shape, object)。 对于任何一种情况,当你调用super(Shape, cls)时,它会忽略mro序列中的所有内容,包括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)

因此,在这种特殊情况下,您可以定义一个新的mixin类来修改形状的构造行为,您可以使用专门的三角形和矩形继承现有的但不同的构造。

答案 1 :(得分:11)

在发布我的问题后,我继续寻找解决方案,找到一种方法来解决看起来有点像黑客的问题。它不如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

答案 2 :(得分:0)

我无法在我安装的任何一个Python解释器中重现这种行为,所以这是一种猜测。然而...

__init__被调用两次,因为您正在初始化两个对象:原始Shape对象,然后是其中一个子类。如果您更改了__init__,那么它也会打印正在初始化的对象的类,您将看到这一点。

print type(self), "init called"

这是无害的,因为原始Shape将被丢弃,因为您没有在__new__()中返回对它的引用。

由于调用一个函数在语法上相同来实例化一个类,你可以将它更改为一个函数而不改变其他任何东西,我建议你这样做。我不明白你的不情愿。