使用类的__new__方法作为工厂:__init__被调用两次

42 投票
3 回答
19815 浏览
提问于 2025-04-16 17:22

我在使用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 个回答

-2

我其实在我安装的两个Python解释器中都无法重现这个行为,所以这只是我的猜测。不过……

__init__被调用了两次,是因为你在初始化两个对象:一个是原始的Shape对象,另一个是它的一个子类。如果你修改你的__init__方法,让它也打印出正在初始化的对象的类名,你就会看到这一点。

print type(self), "init called"

这并没有什么坏处,因为原始的Shape对象会被丢弃,因为你在__new__()中没有返回对它的引用。

由于调用一个函数在语法上和实例化一个类是一样的,你可以把这个改成一个函数,而不需要改变其他任何东西,我建议你就这么做。我不明白你为什么不愿意这样做。

15

在我发问之后,我继续寻找解决办法,发现了一种看起来有点像“黑科技”的解决方法。虽然这个方法不如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
70

当你在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)

这样可以创建一个 RectangleTriangle,而不会触发 __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)

所以在这种特定情况下,你可以定义一个新的混合类,来修改形状的构造行为,你可以让专门的三角形和矩形从现有的类继承,但构造方式不同。

撰写回答