源码剖析-类继承机制与属性查找

前言

面向对象的编程思想对于初学者来说是一大挑战!学了好几个月的童鞋,写代码还是像写作文一样的感觉。内心知道面向对象很好,有诸多的优点,但是还是非常的排斥。排斥的原因无非就是不熟悉、不理解。这也难怪,人总是喜欢做自己擅长的事情。那么今天咱们就来说一下面向对象里面的继承与属性查找

image

本期推送整理了初学者可能会用到的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 基类型对象:

image

现在,实例化一个 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 字段,子类与父类被串在一起,形成一条继承链:

image

大脑一阵缺氧…咱们继续

image

多继承

在多继承场景,故事又是怎样的呢?我们接着扩展前面的例子:

`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 ,表明警犬同时具有猎犬以及搜救犬的特质:

image

接着,我们扒开负责创建 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 字段,它便保存着基类列表:

image

在 Python 层可以访问类 basebases 属性,印证了底层字段 tp_base 以及 tp_bases 的作用:

`>>> PoliceDog.__base__

属性查找

我还有很多头发,我还能继续学习

image

我们或多或少都知道,子类没有定义的属性,应该沿着继承链到父类中查找:

`>>> s = Sleuth()
>>> s.play()
playing
>>> s.run()
running
复制代码`

猎犬类没有定义 play 方法,调用了父类 Dog 中定义的;run 方法也没有定义,则调用了祖类 Animal 中定义的。

简单的单继承场景比较好理解,多继承场景就相对复杂一点。我们先来观察 PoliceDog 的行为,看能否得到一些启发。

image

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
复制代码`

image

根据继承关系,我们可以得到以下结论:

  • 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 函数出发,顺藤摸瓜。印证过程应该不难,因篇幅关系就不过度展开了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值