隐式调用父类初始化器

7 投票
4 回答
4491 浏览
提问于 2025-04-15 19:51
class A(object):
    def __init__(self, a, b, c):
        #super(A, self).__init__()
        super(self.__class__, self).__init__()


class B(A):
    def __init__(self, b, c):
        print super(B, self)
        print super(self.__class__, self)
        #super(B, self).__init__(1, b, c)
        super(self.__class__, self).__init__(1, b, c)

class C(B):
    def __init__(self, c):
        #super(C, self).__init__(2, c)
        super(self.__class__, self).__init__(2, c)
C(3)

在上面的代码中,被注释掉的 __init__ 调用看起来是大家普遍接受的“聪明”方式来进行父类的初始化。不过,如果类的层级结构可能会改变,我最近一直在使用没有注释的那种写法。

在上面的层级结构中,调用 B 的父类构造函数时,发现 B.__init__ 又被调用了一次,而 self.__class__ 实际上是 C,而不是我一直认为的 B

有没有什么方法可以在 Python 2.x 中,在调用父类构造函数时,保持正确的 MRO(即以正确的顺序初始化所有父类),而不需要直接指定当前类(在 super(B, self).__init__(1, b, c) 中的 B)?

4 个回答

1

你的代码和方法解析顺序没有关系。方法解析顺序主要是在多重继承的情况下才会涉及,而你的例子并不是这种情况。你的代码其实是错误的,因为你假设 self.__class__ 和定义方法的类是同一个类,这个假设是错的:

>>> class A(object):
...     def __init__(self):
...         print self.__class__
... 
>>> 
>>> class B(A):
...     def __init__(self):
...         A.__init__(self)
... 
>>> B()
<class '__main__.B'>
<__main__.B object at 0x1bcfed0>
>>> A()
<class '__main__.A'>
<__main__.A object at 0x1bcff90>
>>> 

所以当你应该调用:

super(B, self).__init__(1, b, c)

你实际上调用的是:

# super(self.__class__, self).__init__(1, b, c)
super(C, self).__init__(1, b, c)

编辑:试着更好地回答这个问题。

class A(object):
    def __init__(self, a):
        for cls in self.__class__.mro():
            if cls is not object:
                cls._init(self, a)
    def _init(self, a):
        print 'A._init'
        self.a = a

class B(A):
    def _init(self, a):
        print 'B._init'

class C(A):
    def _init(self, a):
        print 'C._init'

class D(B, C):
    def _init(self, a):
        print 'D._init'


d = D(3)
print d.a

打印结果是:

D._init
B._init
C._init
A._init
3

(这是一个修改过的 模板方法模式 的版本。)

现在父类的方法确实是隐式调用的,但我得同意 Python 的哲学,明确调用比隐式调用要好,因为这样代码更易读,而且收益也不大。不过要注意,所有的 __init__ 方法都有相同的参数,你不能完全忽视父类,我也不建议这样做。

对于单一继承,比较好的做法是明确调用父类的方法,而不是使用 super。这样你就不需要 指定当前类的名字,但仍然需要关注父类是谁。

推荐阅读的内容有: Python 的 super 是如何正确工作的 以及在那个问题中提到的链接,特别是 Python 的 Super 很巧妙,但你不能使用它

如果继承层次结构可能会改变,那就是设计不好的表现,会对使用这段代码的所有部分产生影响,这种情况不应该被鼓励。

编辑 2

我想到另一个例子,但它使用了 metaclass。Urwid 库 使用 metaclass 来存储一个属性 __super,这样你只需要访问这个属性。

例如:

>>> class MetaSuper(type):
...     """adding .__super"""
...     def __init__(cls, name, bases, d):
...         super(MetaSuper, cls).__init__(name, bases, d)
...         if hasattr(cls, "_%s__super" % name):
...             raise AttributeError, "Class has same name as one of its super classes"
...         setattr(cls, "_%s__super" % name, super(cls))
... 
>>> class A:
...  __metaclass__ = MetaSuper
...  def __init__(self, a):
...   self.a = a
...   print 'A.__init__'
... 
>>> class B(A):
...  def __init__(self, a):
...   print 'B.__init__'
...   self.__super.__init__(a)
... 
>>> b = B(42)
B.__init__
A.__init__
>>> b.a
42
>>> 
1

也许你在寻找的是元类?

class metawrap(type):
    def __new__(mcs,name, bases, dict):
        dict['bases'] = bases
        return type.__new__(mcs,name,bases,dict)

class A(object):
    def __init__(self):
        pass
    def test(self):
        print "I am class A"

class B(A):
    __metaclass__ = metawrap
    def __init__(self):
        pass
    def test(self):
        par = super(self.bases[0],self)
        par.__thisclass__.test(self)
foo = B()
foo.test()

这段代码会打印出“I am class A”

元类的作用是重新定义B类的创建过程(不是对象的创建),确保每个B对象的内置字典里都有一个基类数组,这样你就可以找到B类的所有父类了。

3

简单来说:不行,在Python 2.x中,没有办法自动调用正确的__init__方法,并且传入正确的参数和父类。

顺便提一下,这里展示的代码是错误的:如果你使用super().__init__,那么你所有的类都必须在它们的__init__方法中有相同的参数。如果不这样做,当你引入一个使用多重继承的新子类时,你的代码可能会停止工作。

想了解更多这个问题的详细信息(还有图片),可以查看http://fuhm.net/super-harmful/

撰写回答