父类调用子类的属性_源码剖析类继承机制与属性查找

单继承

我们在对象模型中,通过 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 基类型对象:

89b4649616dc4851a023f8c079065490.png

现在,实例化一个 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 ()
4 LOAD_CONST 1 ('Animal')
6 MAKE_FUNCTION 0
8 LOAD_CONST 1 ('Animal')
10 CALL_FUNCTION 2
12 STORE_NAME 0 (Animal)

5 14 LOAD_BUILD_CLASS
16 LOAD_CONST 2 ()
18 LOAD_CONST 3 ('Dog')
20 MAKE_FUNCTION 0
22 LOAD_CONST 3 ('Dog')
24 LOAD_NAME 0 (Animal)
26 CALL_FUNCTION 3
28 STORE_NAME 1 (Dog)

11 30 LOAD_BUILD_CLASS
32 LOAD_CONST 4 ()
34 LOAD_CONST 5 ('Sleuth')
36 MAKE_FUNCTION 0
38 LOAD_CONST 5 ('Sleuth')
40 LOAD_NAME 1 (Dog)
42 CALL_FUNCTION 3
44 STORE_NAME 2 (Sleuth)
46 LOAD_CONST 6 (None)
48 RETURN_VALUE

由上一小节,我们知道 LOAD_BUILD_CLASS 字节码用于加载 __build_class__ 函数,它创建类对象,接口如下:

>>> help(__build_class__)
Help on built-in function __build_class__ in module builtins:

__build_class__(...)
__build_class__(func, name, *bases, metaclass=None, **kwds) -> class

Internal helper function used by the class statement.

__build_class__ 相关参数如下:

  • func ,用于初始化类属性空间的可调动对象,由类代码块生成;

  • name ,类名;

  • bases ,基类,可以为多个;

由此可见,创建子类时,需要将父类作为 bases 参数传给 __build_class__ 函数。

创建 Animal 类时,由于没有显式指定继承关系,因此没有给 __build_class__ 函数传递任何基类:

  1           0 LOAD_BUILD_CLASS
2 LOAD_CONST 0 (<code object Animal at 0x109b90810, file "", line 1>)
4 LOAD_CONST 1 ('Animal')
6 MAKE_FUNCTION 0
8 LOAD_CONST 1 ('Animal')
10 CALL_FUNCTION 2
12 STORE_NAME 0 (Animal)

这时, __build_class__ 函数将默认以 object 为基类创建 Animal 对象。换句话讲,如果自定义类没有显式指定继承关系,将默认继承于 object ,这就是继承链中 object 的由来。

当我们创建 Dog 类时,由于代码中明确指定了从 Animal 继承,偏移量为 24 的那条字节码将 Animal 类加载到运行栈并传给 __build_class__ 函数:

  5          14 LOAD_BUILD_CLASS
16 LOAD_CONST 2 (<code object Dog at 0x109bd1c90, file "", line 5>)
18 LOAD_CONST 3 ('Dog')
20 MAKE_FUNCTION 0
22 LOAD_CONST 3 ('Dog')
24 LOAD_NAME 0 (Animal)
26 CALL_FUNCTION 3
28 STORE_NAME 1 (Dog)

结合对象模型中的知识可知: __build_class__ 函数将基类保存于 PyTypeObject 类型对象的 tp_base 字段中。

通过 tp_base 字段,子类与父类被串在一起,形成一条继承链:

faf5fa45ded5ad07c22db403c4e80488.png

多继承

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

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

8fd252906a96d72a641aead5d1c5768e.png

接着,我们扒开负责创建 PoliceDog 的字节码围观一下:

 21          62 LOAD_BUILD_CLASS
64 LOAD_CONST 8 (<code object PoliceDog at 0x106b81c00, file "", line 21>)
66 LOAD_CONST 9 ('PoliceDog')
68 MAKE_FUNCTION 0
70 LOAD_CONST 9 ('PoliceDog')
72 LOAD_NAME 2 (Sleuth)
74 LOAD_NAME 3 (SnifferDog)
76 CALL_FUNCTION 4
78 STORE_NAME 4 (PoliceDog)
80 LOAD_CONST 10 (None)
82 RETURN_VALUE

注意到,字节码在调用 __build_class__ 函数前,将两个父类 Sleuth 以及 SnifferDog 作为参数按顺序压入栈中。因此,这段字节码等价于:

__build_class__(func, 'PoliceDog', Sleuth, SnifferDog)

这样一来, __build_class__ 函数的 bases 参数将拿到一个由直接父类组成的元组!

在多继承场景,光一个 tp_base 字段不足以保存多个基类, __build_class__ 函数应该将 bases 元组保存在另一个字段中。再次回到 PyTypeObject 源码,不难找出字段 tp_bases 字段,它便保存着基类列表:

3dbf2e1d2099ebf1f0d63d30aac89aea.png

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

>>> PoliceDog.__base__
<class '__main__.Sleuth'>
>>> PoliceDog.__bases__
(<class '__main__.Sleuth'>, <class '__main__.SnifferDog'>)

属性查找顺序

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

>>> s = Sleuth()
>>> s.play()
playing
>>> s.run()
running

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

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

12562eadb424d9970c0d9135ce3fc7b6.png

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__
(<class '__main__.PoliceDog'>, <class '__main__.Sleuth'>, <class '__main__.SnifferDog'>, <class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>)

后续对类进行属性查找时,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

356a06c30d1a603d688e6b50e79b8786.png

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

  • G 优于 C E F

  • C 优于 B

  • E 优于 D

  • B D F 优于 A

  • A 优于 object

然而有些类之间的先后顺序仅靠继承关系是无法确定的, B E 就是其中的例子。这一点都不意外,有向图的拓扑排序结果可能不止一个。

那么,Python 究竟以什么为准呢?

>>> G.__mro__
(<class '__main__.G'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.D'>, <class '__main__.F'>, <class '__main__.A'>, <class 'object'>)

我们直接看结果,可以等到另外两点结论:

  • 拓扑排序是深度优先的,遍历 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、付费专栏及课程。

余额充值