Python class inheritance call order

2019-06-28 05:13发布

There is a famous Python example

class A(object):
    def go(self):
        print("go A go!")

class B(A):
    def go(self):
        super(B, self).go()
        print("go B go!")

class C(A):
    def go(self):
        super(C, self).go()
        print("go C go!")

class D(B,C):
    def go(self):
        super(D, self).go()
        print("go D go!")


d = D()
d.go() 

#go A go!
#go C go!
#go B go!
#go D go!

I have several questions. The first one is B calls A and C calls A so I expect A to appear twice. The second question is about the order.

2条回答
Juvenile、少年°
2楼-- · 2019-06-28 05:19

The class super does not just recover the superclass. It instantiate an object which recovers methods in the context of a given method resolution order. Every class has a mro that you can access through the __mro__ attribute.

D.__mro__ # (D, B, C, A, object)

So when given a class and an instance, super first recovers the mro from that instance. When you try to recover an attribute from the super object, it returns it from the first class following the provided class that has such an attribute.

If you were to implement the behaviour of super in Python, it would look something like this.

class super:
    def __init__(self, cls, instance):
        if not isinstance(cls, type):
            raise TypeError('super() argument 1 must be type')
        if isinstance(instance, cls):
            self.mro = type(instance).__mro__
        elif isinstance(instance, type) and issubclass(instance, cls):
            self.mro = instance.__mro__
        else:
            raise TypeError('super(type, obj): obj must be an instance or subtype of type')

        self.cls = cls
        self.instance = instance

    def __getattr__(self, attr):
        cls_index = self.mro.index(self.cls)

        for supercls in self.mro[cls_index + 1:]:
            if hasattr(supercls, attr): break

        # The actual implementation binds instances to methods before returning
        return getattr(supercls, attr)

So back to your example, when you call super(B, self).go, it recovers the __mro__ of self, which is of type D. It then picks go from the first class following B in the mro that has such an attribute.

So in this case since self.__mro__ is (D, B, C, A, object), the first class following B that has the attribute go is C and not A.

If you want details on how Python determines the mro, then I suggest abarnert's answer.

查看更多
啃猪蹄的小仙女
3楼-- · 2019-06-28 05:33

Since Python 2.3, method resolution has used an algorithm called C3 Linearization (borrowed from Dylan). Wikipedia has a nice article on it.

As the name implies, the idea is to force the method resolution graph to be a straight line, even if the inheritance graph isn't. Which means A is not going to appear twice, by design.

Why? Well, for one thing, it completely avoids the "diamond problem" that plagues multiple inheritance in many other languages. (Or maybe it's more accurate to say that many other languages either ban MI or restrict it to pure "interfaces" because they didn't have a solution to the problem as it exists in C++.)

The original Python explanation—including the motivation behind it—is available in The Python 2.3 Method Resolution Order. This is a bit technical, but worth reading if you're interested.

You might also want to read the original Dylan paper, which goes into more detail about what's wrong with non-linear MRO graphs, and the challenges of coming up with a linearization that's monotonic (i.e., it goes in the order you expect, or at least the order you expect once you get over the fact that it's linear), and so on.

And if you want a deeper understanding of how type() works under the covers, or just want to see what's changed between 2.3 and 3.7 (e.g., the way __mro__ gets created and updated—although magic 3.x super is elsewhere), there's really no better place than the CPython source.

查看更多
登录 后发表回答