Python开发-030_继承优先级c3算法与mro
我们在4-28 封装继承与多态
介绍过了继承的概念,继承存在意义就是将公共的方法提取到父类中,有利于增加代码重用性,也提到了python是支持多继承的编程语言,而继承的顺序,简单的是遵循先左后右模式,但如果较为复杂就没有办法判断了,所以我们在这里介绍Python解释器是如何判断继承优先级的,这就是接下来讲到的c3算法
1 C3算法
由简入奢易,我们从基础的继承关系来进行分析
1.1 案例1
代码如下:
class D(object):
def demo(self):
print("class D.demo")
class C(object):
def demo(self):
print("class C.demo")
class B(D):
pass
class A(B, C):
pass
obj = A()
obj.demo()
优先级分析过程:
# class A 有两个父类:class B、class C,列为算式
# mro(B) 代指B中可能还有父类 A的直系父类[B,C]
mro(A) = [A] + [mro(B),mro(c),[B,C]]
# mro(B) 可分解为 [B,D] mro(c) 可分解为[c]
mro(A) = [A] + [[B,D],[c],[B,C]]
# [[B,c],[c],[B,C]] =》 用第一个列表的第一个函数去找其他列表的非第一个函数,是否有重复,如果没有则将那个函数全部单独拿出来
# 被拿出来的列表[B] 原式子列表[D][C][C],重复过程,拿出[D]=》最后只剩下[C,C]
# 所以 mro(A) = [A,B,D,C]
1.2 案例2
代码如下:
class D(object):
pass
class C(D):
pass
class B(D):
pass
class A(B, C):
pass
优先级分析过程:
mro(A) = [A] + merge(mro(B),mro(C),[B,C])
mro(A) = [A] + merge([B,D],[C,D],[B,C])
# [B] => merge([D],[C,D],[C]) => D在[C,D]第二栏有,则开始筛选C
mro(A) = [A,B] + merge([D],[C,D],[C])
# [C] => 满足列表[1:]没有的条件
mro(A) = [A,B,C] + merge([D],[D],[])
mro(A) = [A,B,C,D]
1.3 案例3
代码如下
class M:
pass
class N:
pass
class E(M):
pass
class G:
pass
class K:
pass
class H(K):
pass
class D(G, H):
pass
class F(M, N):
pass
class P:
pass
class C(E, F):
pass
class B(D, E):
pass
class A(B, C, P):
pass
优先级分析过程:
mro(A) = [A] + [mro(B),mro(C),mro(P),[B,C,P]]
# mro(B) => [B],[D,G,H,K],[E,M][D,E] => [B,D,G,H,K,E,M]
# mro(C) => [C],[E,m],[F,M,N] =>[C,E,F,M,N]
# mro(P) => [P]
mro(A) = [A] + [mro(B),mro(C),mro(P),[B,C,P]] => [A]+[[B,D,G,H,K,E,M],[C,E,F,M,N],[P],[B,C,P]]
mro(A) = [A,B] + [[D,G,H,K,E,M],[C,E,F,M,N],[P],[C,P]]
mro(A) = [A,B,D] + [[G,H,K,E,M],[C,E,F,M,N],[P],[C,P]]
mro(A) = [A,B,D,G] + [[H,K,E,M],[C,E,F,M,N],[P],[C,P]]
mro(A) = [A,B,D,G,H] + [[K,E,M],[C,E,F,M,N],[P],[C,P]]
mro(A) = [A,B,D,G,H,K] + [[E,M],[C,E,F,M,N],[P],[C,P]]
mro(A) = [A,B,D,G,H,K,C] + [[M],[E,F,M,N],[P],[P]]
mro(A) = [A,B,D,G,H,K,C,E] + [[M],[F,M,N],[P],[P]]
mro(A) = [A,B,D,G,H,K,C,E,F] + [[M],[M,N],[P],[P]]
mro(A) = [A,B,D,G,H,K,C,E,F,M] + [[],[N],[P],[P]]
mro(A) = [A,B,D,G,H,K,C,E,F,M,N] + [[],[],[P],[P]]
mro(A) = [A,B,D,G,H,K,C,E,F,M,N,P]
对了,别忘记了,所有类的父类都有object
最后的查找顺序是
A -> B -> D -> G -> H -> K -> C -> E -> F -> M -> N -> P -> object
2 mro方法
c3算法介绍完毕,但是在读代码的时候需要按顺序推理,实在太过麻烦,在python中,为我们提供了一个查找优先级的方法mro()
class M:
pass
class N:
pass
class E(M):
pass
class G:
pass
class K:
pass
class H(K):
pass
class D(G, H):
pass
class F(M, N):
pass
class P:
pass
class C(E, F):
pass
class B(D, E):
pass
class A(B, C, P):
pass
print(A.mro())
'''
[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.D'>, <class '__main__.G'>, <class '__main__.H'>, <class '__main__.K'>, <class '__main__.C'>, <class '__main__.E'>, <class '__main__.F'>, <class '__main__.M'>, <class '__main__.N'>, <class '__main__.P'>, <class 'object'>]
'''
特别补充:一句话搞定继承关系
不知道你是否发现,如果用正经的C3算法规则去分析一个类继承关系有点繁琐,尤其是遇到一个复杂的类也要分析很久。
所以,路飞学城Alex老师根据经验总结了一句话:从左到右,深度优先,大小钻石,留住顶端,基于这句话可以更快的找到继承关系
3 类继承关系的演化史
在python2.2之前,python只支持经典类,经典类并不默认继承object,它的继承关系是从左到右,深度优先,大小钻石,不留顶端
随后由于其他面向对象都支持object的影响,Python也想让类默认继承object,发现原来的经典类不能直接集成集成这个功能,有Bug,object的优先级可能会比其他父类更高
所以,Python决定不再原来的经典类上进行修改了,而是再创建一个新式类来支持这个功能,python2.2就出现了经典类和新式类共存(正式支持是2.3),最终,python3中丢弃经典类,只保留新式类
参考文献:
详细文档:
https://www.python.org/dev/peps/pep-0253/#mro-method-resolution-order-the-lookup-rule
https://www.python.org/download/releases/2.3/mro/
In classic Python, the rule is given by the following recursive function, also known as the left-to-right depth-first rule.
def classic_lookup(cls, name):
if cls.__dict__.has_key(name):
return cls.__dict__[name]
for base in cls.__bases__:
try:
return classic_lookup(base, name)
except AttributeError:
pass
raise AttributeError, name
The problem with this becomes apparent when we consider a "diamond diagram":
class A:
^ ^ def save(self): ...
/ \
/ \
/ \
/ \
class B class C:
^ ^ def save(self): ...
\ /
\ /
\ /
\ /
class D
Arrows point from a subtype to its base type(s). This particular diagram means B and C derive from A, and D derives from B and C (and hence also, indirectly, from A).
Assume that C overrides the method save(), which is defined in the base A. (C.save() probably calls A.save() and then saves some of its own state.) B and D don't override save(). When we invoke save() on a D instance, which method is called? According to the classic lookup rule, A.save() is called, ignoring C.save()!
This is not good. It probably breaks C (its state doesn't get saved), defeating the whole purpose of inheriting from C in the first place.
Why was this not a problem in classic Python? Diamond diagrams are rarely found in classic Python class hierarchies. Most class hierarchies use single inheritance, and multiple inheritance is usually confined to mix-in classes. In fact, the problem shown here is probably the reason why multiple inheritance is unpopular in classic Python.
Why will this be a problem in the new system? The 'object' type at the top of the type hierarchy defines a number of methods that can usefully be extended by subtypes, for example __getattr__().
(Aside: in classic Python, the __getattr__() method is not really the implementation for the get-attribute operation; it is a hook that only gets invoked when an attribute cannot be found by normal means. This has often been cited as a shortcoming -- some class designs have a legitimate need for a __getattr__() method that gets called for all attribute references. But then of course this method has to be able to invoke the default implementation directly. The most natural way is to make the default implementation available as object.__getattr__(self, name).)
Thus, a classic class hierarchy like this:
class B class C:
^ ^ def __getattr__(self, name): ...
\ /
\ /
\ /
\ /
class D
will change into a diamond diagram under the new system:
object:
^ ^ __getattr__()
/ \
/ \
/ \
/ \
class B class C:
^ ^ def __getattr__(self, name): ...
\ /
\ /
\ /
\ /
class D
and while in the original diagram C.__getattr__() is invoked, under the new system with the classic lookup rule, object.__getattr__() would be invoked!
Fortunately, there's a lookup rule that's better. It's a bit difficult to explain, but it does the right thing in the diamond diagram, and it is the same as the classic lookup rule when there are no diamonds in the inheritance graph (when it is a tree).
3.1 python2与python3面向对象中的区别
-
Python2:
-
经典类,未继承object类型。【从左到右,深度优先,大小钻石,不留顶端】
class Foo: pass
-
新式类,直接获取间接继承object类型。【从左到右,深度优先,大小钻石,留住顶端 -- C3算法】
class Foo(object): pass
或
class Base(object): pass class Foo(Base): pass
-
-
Python3
-
新式类,丢弃了经典类只保留了新式类。【从左到右,深度优先,大小钻石,留住顶端 -- C3算法】
class Foo: pass class Bar(object): pass
-