前言
面向对象的编程思想对于初学者来说是一大挑战!学了好几个月的童鞋,写代码还是像写作文一样的感觉。内心知道面向对象很好,有诸多的优点,但是还是非常的排斥。排斥的原因无非就是不熟悉、不理解。这也难怪,人总是喜欢做自己擅长的事情。那么今天咱们就来说一下面向对象里面的继承与属性查找
本期推送整理了初学者可能会用到的Python资料,含有书籍/视频/在线文档和编辑器/源
代码,关于Python
的安装qun:850973621
实战
单继承
下面有一段代码,通过 Dog 与 Sleuth 类来讲解类继承关系。为了进一步讨论类继承机制,我们进一步扩充:
`class Animal: def run(self): print('running') class Dog(Animal): def yelp(self): print('woof') def play(self): print('playing') class Sleuth(Dog): def yelp(self): print('WOOF!') def hunt(self): print('hunting') 复制代码`
通过引入 Animal 类,我们得到一条包含 3 个类的继承链,继承链结束语 object 基类型对象:
现在,实例化一个 Sleuth 对象,它可以调用自己定义的方法,例如 hunt :
`>>> s = Sleuth() >>> s.hunt() hunting 复制代码`
当然了,由于 Sleuth 类继承于 Dog 类,因此 Sleuth 对象也可以调用 Dog 定义的方法,例如 play :
`>>> s.play() playing 复制代码`
Sleuth 类通过 Dog 类间接继承于 Animal 类,因此它也可以调用 Animal 定义的方法:
`>>> s.run() running 复制代码`
如果子类对父类中的方法不满意,还可以进行方法重写。猎犬吠声与普通狗有所不同,我们可以为 Sleuth 类重写 yelp 方法,以大写突出吠声的威武雄壮。这样一来,Sleuth 实例对象将执行 Sleuth 类中定义的 yelp 方法版本:
`>>> s.yelp() WOOF! 复制代码`
那么,Python 虚拟机内部是如何实现继承机制的呢?我们接着到字节码中寻找秘密。
对以上例子进行编译,我们可以得到这样的字节码:
`1 0 LOAD_BUILD_CLASS 2 LOAD_CONST 0 (
build_class 相关参数如下:
- func ,用于初始化类属性空间的可调动对象,由类代码块生成;
- name ,类名;
- bases ,基类,可以为多个;
由此可见,创建子类时,需要将父类作为 bases 参数传给 build_class 函数。
创建 Animal 类时,由于没有显式指定继承关系,因此没有给 build_class 函数传递任何基类:
` 1 0 LOAD_BUILD_CLASS 2 LOAD_CONST 0 (
这时, build_class 函数将默认以 object 为基类创建 Animal 对象。换句话讲,如果自定义类没有显式指定继承关系,将默认继承于 object ,这就是继承链中 object 的由来。
当我们创建 Dog 类时,由于代码中明确指定了从 Animal 继承,偏移量为 24 的那条字节码将 Animal 类加载到运行栈并传给 build_class 函数:
` 5 14 LOAD_BUILD_CLASS 16 LOAD_CONST 2 (
结合对象模型中的知识可知: build_class 函数将基类保存于 PyTypeObject 类型对象的 tp_base 字段中。
通过 tp_base 字段,子类与父类被串在一起,形成一条继承链:
大脑一阵缺氧…咱们继续
多继承
在多继承场景,故事又是怎样的呢?我们接着扩展前面的例子:
`class Animal: def run(self): print('running') class Dog(Animal): def yelp(self): print('woof') def play(self): print('playing') class Sleuth(Dog): def yelp(self): print('WOOF!') def hunt(self): print('hunting') class SnifferDog(Dog): def search(self): print('searching') class PoliceDog(Sleuth, SnifferDog): def patrol(self): print('patroling') 复制代码`
这个例子引入搜救犬类 SnifferDog ,继承于普通狗 Dog ;警犬类 PoliceDog 同时继承于猎犬类 Sleuth 以及搜救犬类 SnifferDog ,表明警犬同时具有猎犬以及搜救犬的特质:
接着,我们扒开负责创建 PoliceDog 的字节码围观一下:
` 21 62 LOAD_BUILD_CLASS 64 LOAD_CONST 8 (
注意到,字节码在调用 build_class 函数前,将两个父类 Sleuth 以及 SnifferDog 作为参数按顺序压入栈中。因此,这段字节码等价于:
`__build_class__(func, 'PoliceDog', Sleuth, SnifferDog) 复制代码`
这样一来, build_class 函数的 bases 参数将拿到一个由直接父类组成的元组!
在多继承场景,光一个 tp_base 字段不足以保存多个基类, build_class 函数应该将 bases 元组保存在另一个字段中。再次回到 PyTypeObject 源码,不难找出字段 tp_bases 字段,它便保存着基类列表:
在 Python 层可以访问类 base 和 bases 属性,印证了底层字段 tp_base 以及 tp_bases 的作用:
`>>> PoliceDog.__base__
属性查找
我还有很多头发,我还能继续学习
我们或多或少都知道,子类没有定义的属性,应该沿着继承链到父类中查找:
`>>> s = Sleuth() >>> s.play() playing >>> s.run() running 复制代码`
猎犬类没有定义 play 方法,调用了父类 Dog 中定义的;run 方法也没有定义,则调用了祖类 Animal 中定义的。
简单的单继承场景比较好理解,多继承场景就相对复杂一点。我们先来观察 PoliceDog 的行为,看能否得到一些启发。
PoliceDog 类定义了 patrol 方法,PoliceDog 实例便可以调用该方法,这很好理解:
`>>> pd = PoliceDog() >>> pd.patrol() patroling 复制代码`
虽然 PoliceDog 类没有定义 hunt 方法,但其父类 Sleuth 定义了,因此实例也可以调用方法:
`>>> pd.hunt() hunting 复制代码`
虽然父类 Sleuth 以及 祖类 Dog 均定义了 yelp 方法,PoliceDog 实例会选择血缘较近的版本,即 Sleuth 版本:
`>>> pd.yelp() WOOF! 复制代码`
Sleuth 覆盖了其父类 Dog 中的 yelp 方法,这在面向对象中称为 覆写 ( overwritting ),search 方法也是同理:
`>>> pd.search() searching 复制代码`
至此,我们可以总结出一条非常重要的规则,在属性查找的过程中,子类版本优先级总是高于父类。举个例子, Sleuth 类定义的版本比 Dog 类优先级更高。
如果你对常用数据结构比较熟悉的话,可能已猜到其中的玄机:属性查找的顺序就是对类继承关系图的 拓扑排序 !拓扑排序恰好可以保证有向图中有连接关系节点间的先后顺序。
实际上,Python 虚拟机在创建自定义类对象时,对继承关系图进行拓扑排序,并将排序结果保存于 PyTypeObject 中的 tp_mro 字段中,该字段可通过 mro 属性访问:
`>>> PoliceDog.__mro__ (
后续对类进行属性查找时,Python 将依照 mro 中保存的顺序逐一查找。这是以空间换时间的又一典型例子,有效避免重复排序导致的计算开销。
最后,我们回过头来考察一个更复杂的多继承场景:
`class A: pass class B(A): pass class C(B): pass class D(A): pass class E(D): pass class F(A): pass class G(C, E, F): pass 复制代码`
根据继承关系,我们可以得到以下结论:
-
G 优于 C E F
-
C 优于 B
-
E 优于 D
-
B D F 优于 A
-
A 优于 object
然而有些类之间的先后顺序仅靠继承关系是无法确定的, B E 就是其中的例子。这一点都不意外,有向图的拓扑排序结果可能不止一个。
那么,Python 究竟以什么为准呢?
`>>> G.__mro__ (
我们直接看结果,可以等到另外两点结论:
- 拓扑排序是深度优先的,遍历 C 之后,继续遍历 B,而不是 E ;
- 分支遍历顺序由基类列表定义顺序决定,因此 C E F 一定按照这个顺序遍历;
实际上,Python 内部采用 C3算法 ,根据继承关系图生成一个满足以上约束的拓扑排序。如果你对源码比较感兴趣的话,可以从 build_class 函数出发,顺藤摸瓜。印证过程应该不难,因篇幅关系就不过度展开了。