3

super()有很多很棒的资源,包括this很棒的博客文章,弹出很多,以及关于堆栈溢出的很多问题。不过,我觉得他们都停下来解释它在最常见的情况下如何工作(使用任意的继承图),以及引擎盖下正在发生的事情。在一般情况下,Python的super()实际上是如何工作的?

考虑菱形继承的这个简单的例子:

class A(object): 
    def foo(self): 
     print 'A foo' 

class B(A): 
    def foo(self): 
     print 'B foo before' 
     super(B, self).foo() 
     print 'B foo after' 

class C(A): 
    def foo(self): 
     print 'C foo before' 
     super(C, self).foo() 
     print 'C foo after' 

class D(B, C): 
    def foo(self): 
     print 'D foo before' 
     super(D, self).foo() 
     print 'D foo after' 

如果您对Python的规则从源方法解析顺序读取像this或查找的wikipedia page为C3线性化,你会看到MRO必须是(D, B, C, A, object)。这当然是由D.__mro__证实:

(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>) 

而且

d = D() 
d.foo() 

打印

D foo before 
B foo before 
C foo before 
A foo 
C foo after 
B foo after 
D foo after 

其MRO匹配。但是,考虑到以上的super(B, self).foo()实际上称为C.foo,而在b = B(); b.foo()中,它将直接转到A.foo。正如有时教导的那样,清楚地使用super(B, self).foo()不仅仅是A.foo(self)的简写。

super()然后很明显知道之前的调用以及该链试图遵循的整体MRO。我可以看到两种方式可以实现。第一种方法是将super对象本身作为self参数传递给链中的下一个方法,它将像原始对象那样工作,但也包含此信息。但是,这似乎也会破坏很多东西(super(D, d) is d是错误的),并且通过做一点实验我可以看出情况并非如此。

另一种选择是有某种全局上下文来存储MRO和其中的当前位置。我想super的算法如下所示:

  1. 目前是否有我们正在使用的上下文?如果没有,则创建一个包含队列的文件。获取类参数的MRO,将除第一个元素之外的所有元素都推送到队列中。
  2. 从当前上下文的MRO队列中弹出下一个元素,在构造super实例时将其用作当前类。
  3. 当从super实例访问某个方法时,请在当前类中查找并使用相同的上下文调用它。

然而,这并不能解释像使用不同的基类作为第一个参数来调用super,甚至调用它不同的方法奇怪的事情。我想知道这一般的算法。另外,如果这个上下文存在,我可以检查它吗?我可以使用它吗?可怕的想法,但Python通常希望你成为一个成熟的成年人,即使你不是。

这也引入了很多设计考虑因素。如果我写B只考虑它与A的关系,稍后有人写C而第三个人写D,我的B.foo()方法必须以与C.foo()兼容的方式呼叫super,即使它在当时不存在我写的!如果我希望我的课程易于扩展,我将需要对此进行说明,但我不确定它是否比仅确保foo的所有版本具有相同签名更加复杂。在super的调用之前或之后,即使它仅考虑B的基类没有任何区别,也存在何时在代码之前或之后放置代码的问题。

+0

“我不确定它是否比简单地确保foo的所有版本都具有相同的签名更复杂”,这是超级普遍使用的要求(尽管您可以使用kwargs来解决它) –

+0

您需要相同的签名或使用'* args,** kwargs'来扫描任何正在传播的东西。 “super”的第一个参数是*上面的类*它应该寻找方法 - 通常你想要一个在当前的那个之上,因此'super(ThisClass,self)'。 *“'super(B,self).foo()'不是简单的'A.foo(self)'”* - no的快捷方式,它会在'B'后面的MRO中调用下一个'foo'实现,绑定它“自我”。您是否看过侧边栏中的**相关**问题,有相当多的相关问题。 – jonrsharpe

+0

@jonrsharpe - 我已经看过相关的问题,但正如我所说,他们真的只处理简化版本,而不是一般情况。我最近了解到'super(B,self).foo()'不是'A.foo(self)'的快捷方式,因为我一直在对自己进行更多的介绍,但实际上这种方法通常是这样教的。当然,在绝大多数单一继承的情况下,这并不重要,但它通常意味着使用具有明确参数的'super(cls,self).__ init__'是不适当的,因为它可以回到'对象“。 – JaredL

回答

7

super() is then obviously aware of the previous calls before it

不是这样。当你做super(B, self).foo时,super知道MRO,因为那只是type(self).__mro__,并且它知道它应该在B之后立即开始在MRO中寻找foo。粗略纯Python相当于将

class super(object): 
    def __init__(self, klass, obj): 
     self.klass = klass 
     self.obj = obj 
    def __getattr__(self, attrname): 
     classes = iter(type(self.obj).__mro__) 

     # search the MRO to find self.klass 
     for klass in classes: 
      if klass is self.klass: 
       break 

     # start searching for attrname at the next class after self.klass 
     for klass in classes: 
      if attrname in klass.__dict__: 
       attr = klass.__dict__[attrname] 
       break 
     else: 
      raise AttributeError 

     # handle methods and other descriptors 
     try: 
      return attr.__get__(self.obj, type(self.obj)) 
     except AttributeError: 
      return attr 

If I wrote B thinking only of its relation to A, then later someone else writes C and a third person writes D, my B.foo() method has to call super in a way that is compatible with C.foo() even though it didn't exist at the time I wrote it!

有没有期望,你应该能够从任意类多继承。除非foo专门设计为在多继承情况下由兄弟类重载,否则D不应存在。

+0

在同一个迭代器上的双循环非常微妙。我认为我会更加明确一些,并且使用一个带有标志的for循环来说明目标类是否已经找到(return类也可以放在循环中):'found_klass = False;对于klass类型(self.obj).__ mro__:如果不是found_klass:found_klass = klass是self.klass; elif attrname in klass .__ dict__:attr = klass .__ dict __ [attrname]; try:return attr .__ get __(self.obj,type(self.obj));除了AttributeError:return attr;引发AttributeError'(必要时用换行符和缩进代替分号)。 – Blckknght

+0

谢谢你的出色答案,这段代码真的帮助我理解发生了什么。我不确定我是如何错过它的,但明确地使用'super'调用'B.foo'与直接调用'b.foo()'不同,因为'self'将是'D'的一个实例,而不是'B',所以你可以通过'type(self.obj).__ mro__'而不是'self.klass .__ mro__'得到整个正确的MRO。当然,不需要全球性的国家,我不知道我为什么不这样想。这也解释了当传递'klass'参数不是方法的所有者时会发生什么。 – JaredL

+0

至于第二部分,我同意你不应该能够从任意类多重继承。然而,在某些情况下它可能很有用 - 我一直在制作一些“mixin”类,它们与SQLAlchemy的声明基础一起是多重继承的,现在我意识到我可能不得不回去做一些修改,回来后咬我屁股。 – JaredL

相关问题