Python的super()在通用情况下是如何工作的?

4

关于 super(),有很多优秀的资源可供使用,包括这篇非常出色的博客文章,以及Stack Overflow上的许多问题。然而,我感觉它们都没有充分解释它在最一般的情况下(具有任意继承图)是如何工作的,以及在幕后发生了什么。

考虑这个菱形继承的基本示例:

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中方法解析顺序的规则)或者查看维基百科上的 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

super()会匹配MRO。但是,需要注意的是,在B中,super(B, self).foo()上方实际调用C.foo,而在b = B(); b.foo()中,它将直接转到A.foo。显然,使用super(B, self).foo()并不仅仅是一种快捷方式,就像有时候所教授的那样。

super()显然会意识到它之前的调用以及整个MRO链正在尝试遵循的路径。我可以看到两种方法可以实现这一点。第一种方法类似于将super对象本身作为下一个方法中的self参数传递,这将像原始对象一样工作,但也包含此信息。然而,这似乎也会破坏很多东西(super(D, d) is d为false),通过一些实验,我可以看到这不是事实。

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

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

然而,这不能解释一些奇怪的事情,例如将不同的基类作为对super的第一个参数的调用,甚至调用不同的方法。我想知道这个通用算法。此外,如果该上下文存在于某个位置,我能检查它吗?我能深入挖掘它吗?这当然是可怕的想法,但Python通常预计您是一个成熟的成年人,即使您不是。

这还引入了许多设计考虑因素。如果我编写B时只考虑其与A之间的关系,那么稍后有人编写了C,第三个人编写了D,我的B.foo()方法必须以与C.foo()兼容的方式调用super,即使在我编写它时它不存在!如果我希望我的类易于扩展,我需要考虑这一点,但我不确定是否比仅确保所有版本的foo具有相同的签名更为复杂。还有一个问题是何时在调用super之前或之后放置代码,即使考虑到B的基类可能不会造成任何影响。


我不确定它是否比仅确保所有版本的foo具有相同的签名更复杂,这是使用super通常需要满足的要求(尽管您可以使用kwargs绕过它)。 - Chad S.
你需要拥有相同的签名或使用 *args, **kwargs 来清理传递的其他东西。super 的第一个参数是它应该查找方法的上层类 - 通常你想要当前类的直接上一级,因此使用 super(ThisClass, self)。"super(B, self).foo() 不仅仅是 A.foo(self) 的快捷方式" - 它调用 B 之后 MRO 中下一个 foo 实现,并将其绑定到 self。你是否已经看了侧边栏中的相关问题,其中有相当多的问题与此相关。 - jonrsharpe
@jonrsharpe - 我已经查看了相关问题,但正如我所说,它们只涉及简化版本,而不是一般情况。最近我学到了super(B, self).foo()并不是A.foo(self)的快捷方式,因为我自己更多地在学习它,但通常确实是这样教授的。当然,在单继承的大多数情况下,这并不重要,但这意味着一般情况下不应该使用带有显式参数的super(cls, self).__init__,因为它最终会回到object - JaredL
它被这样教授是因为在单继承情况下,它就是这样工作的,而这涵盖了很多领域。你会发现很多学习都是这样的,出于必要性,我们从简化和有时是彻头彻尾的谎言开始! - jonrsharpe
1个回答

9

super()显然知道它之前的调用

事实并非如此。当你执行super(B, self).foo时,super知道MRO是因为这只是type(self).__mro__,并且它知道应该从MRO中紧接在B之后的点开始寻找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

如果我在编写B时只考虑其与A的关系,然后稍后其他人编写了C,第三个人编写了D,那么我的B.foo()方法必须以与C.foo()兼容的方式调用super,即使在我编写它的时候并不存在!没有任何期望你应该能够从任意类中多重继承。除非foo专门设计为在多重继承情况下由兄弟类进行重载,否则D不应存在。

那个在同一个迭代器上的双重循环非常微妙。我认为我会更加明确,使用一个带有标志的单个for循环来表示是否已经找到目标类(return语句也可以放在循环内):found_klass = False; for klass in type(self.obj).__mro__: if not found_klass: found_klass = klass is self.klass; elif attrname in klass.__dict__: attr = klass.__dict__[attrname]; try: return attr.__get__(self.obj, type(self.obj)); except AttributeError: return attr; raise AttributeError(必要时换行和缩进)。 - Blckknght
非常感谢您提供的出色答案,这段代码确实帮助我理解了正在发生的事情。我不确定我是怎么错过它的,但显然使用super调用B.foo与直接调用b.foo()不同,因为self将是D的实例而不是B的实例,所以您可以通过type(self.obj).__mro__而不是self.klass.__mro__来获取整个正确的MRO。当然,不需要任何全局状态,我不知道我为什么会想到其他的东西。这也解释了当传递的klass参数不是方法所有者时会发生什么。 - JaredL
至于第二部分,我同意你不应该期望能够从任意类中进行多重继承。然而,在某些情况下它是有用的 - 我一直在制作一些“mixin”类,旨在与SQLAlchemy的声明性基础一起进行多重继承,现在我意识到我可能需要回去做一些更改,以便事情不会在以后反咬我一口。 - JaredL
super(type[, object-or-type])。根据Python文档,super()应该使用type.__mro__而不是type(object).__mro__,但这并不能解决问题。那么,文档是错误的吗?“当您执行super(B, self).foo时,super知道MRO,因为那只是type(self).__mro__。”这一点从哪里得出的呢? - MMMMMCCLXXVII
根据文档,它应该使用第一个参数的__mro__,但是文档在这里是错误的。第一个参数的__mro__完全被忽略了。 - user2357112

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接