Python 进阶技巧: 类与继承
本文主要介绍有关 Python 中类(class)与继承(inheritance)的一些高级使用知识与技巧, 包括:
-
与类相关的若干内置函数
-
多继承下的 MRO 序列与 C3 序列化方法
-
super 类的使用
-
魔术方法与类的实例化过程
在看公司一份超大的历史遗留项目时, 发现不仅仅有装饰器、描述器、大量的父子继承关系(实不相瞒, 太多的继承已经使得这份代码非常难以维护)、甚至还有for break这种此前从未见过的语法..
因为入门 Python 还是靠我的本科毕设写了一些简单的 Python 机器学习/神经网络代码, 还远称不上精通.. 因此也是顺带着去看了一些 Python 的高级编程技巧, 这种纯动态的、对OOP支持良好的、万物皆对象的语言确实是给我带来了很大的新奇感, 在我的 C & shell & Rust 的主要编程语系中掀起了不小的波兰.
所以就发现 Python 语言的特性还蛮好玩的, 而且 CPython 是基于 C 写的, 让人又多了几分亲切感.
综合上述的考量, 可能会稍微对 Python 的各种特性和技巧进行一个深入的研究. :)
两个相关的内置函数
Of course, a language feature would not be worthy of the name “class” without supporting inheritance.
继承是OOP的一大特性, Python 拥有两个相关的内置函数用于判断继承关系:
issubclass
和 isinstance
:
-
使用 isinstance() 来检查一个实例的类型: isinstance(obj, int) 仅会在 obj.__class__ 为 int 或某个派生自 int 的类时为 True.
-
使用 issubclass() 来检查类的继承关系: issubclass(bool, int) 为 True, 因为 bool 是 int 的子类. 但是, issubclass(float, int) 为 False, 因为 float 不是 int 的子类.
参考: 9. Classes — Python 3.12.1 documentation
MRO 与 C3 算法
C3算法(C3 superclass linearization)算法主要用于确定多重继承时, 子类应该继承哪一个父类的方法, 即方法解析顺序(Method Resolution Order, MRO).
C3 superclass linearization is called C3 because it “is consistent with three properties”:
- a consistent extended precedence graph, (保持继承拓扑图的一致性)
- preservation of local precedence order, and (保证局部优先原则; 比如A继承C, C继承B, 那么A读取父类方法, 应该优先使用C的方法而不是B的方法)
- fitting a monotonicity criterion. (保证单调性原则, 即子类不改变父类的方法搜索顺序)
在Python2.3之前是基于深度优先算法, 为了解决原来基于dfs算法不满足本地优先级, 和单调性以及继承不清晰的问题, 从Python2.3起应用了新的C3算法.
C3 序列化算法原理
若类型B继承了多个基类:
1class B(A1,A2,...,An)
则 B 的 mro 序列可以通过以下递归式获得:
1mro(B) = [B] + merge(mro(A1), mro(A2),...,mro(An), [A1,A2,...,An])
其中 merge是一个函数操作, 接收多个序列作为参数, 并返回一个参数; 其工作流程为:
-
遍历序列, 检查当前遍历到的序列的第一个元素:
-
若其在其他序列中也是第一个元素, 或不在其他序列出现, 则将该元素append到返回序列中, 并从所有merge操作序列中删除;
-
不然, 则基序遍历, 重复上述操作
-
-
不断执行上述的遍历操作, 直到 merge 操作序列为空
C3 算法实例
1class A(object):pass
2class B(object):pass
3class C(object):pass
4class E(A,B):pass
5class F(B,C):pass
6class G(E,F):pass
在手算的时候, 不能从下往上算, 因为纸笔难做到记忆化和递归, 从基类不断往下算比较好
A, B, C的 mro 序列易知为 [A,O]
、[B,O]
、[C,O]
E 的 mro 序列为:
1mro(E) = [E] + merge(mro(A), mro(B), [A,B])
2 = [E] + merge([A,O], [B,O], [A,B])
3 = [E,A] + merge([O], [B,O], [B])
4 = [E,A,B] + merge([O], [O])
5 = [E,A,B,O]
类似的也容易算出F和G的mro序列为: [F,B,C,O]
与 [G,E,A,F,B,C,O]
C3 算法 python 实现
talk is cheap, 这份代码的实现可以加深对C3算法的理解.
1def c3MRO(cls):
2 if cls is object:
3 # 讨论假设顶层基类为object,递归终止
4 return [object]
5
6 # 构造C3-MRO算法的总式,递归开始
7 mergeList = [c3MRO(baseCls) for baseCls in cls.__bases__]
8 mergeList.append(list(cls.__bases__))
9 mro = [cls] + merge(mergeList)
10 return mro
11
12
13def merge(inLists):
14 if not inLists:
15 # 若合并的内容为空,返回空list
16 # 配合下文的排除空list操作,递归终止
17 return []
18
19 # 遍历要合并的mro
20 for mroList in inLists:
21 # 取head
22 head = mroList[0]
23 # 遍历要合并的mro(与外一层相同),检查尾中是否有head
24 ### 此处也遍历了被取head的mro,严格地来说不符合标准算法实现
25 ### 但按照多继承中地基础规则(一个类只能被继承一次),
26 ### head不可能在自己地尾中,无影响,若标准实现,反而增加开销
27 for cmpList in inLists[inLists.index(mroList) + 1:]:
28 if head in cmpList[1:]:
29 break
30 else:
31 # 筛选出好head
32 nextList = []
33 for mergeItem in inLists:
34 if head in mergeItem:
35 mergeItem.remove(head)
36 if mergeItem:
37 # 排除空list
38 nextList.append(mergeItem)
39 # 递归开始
40 return [head] + merge(nextList)
41 else:
42 # 无好head,引发类型错误
43 raise TypeError
此外, 使用 python 自带的 .mro()
方法也可以获取 mro 序列.
参考:
super 的使用
明白了 mro 的基本原理后就容易理解如何使用 super
了.
这里需要注意, super 是一个类, 而不是一个方法!
super(class, obj)
接收两个参数, 返回的是 obj
的 mro 序列中的 class
类的父类, 其中 obj 可以是一个类也可以是一个实例.
当不传入参数给 super
时, 其将(1)寻找自己在哪个类中被定义, 并把类名作为第一个参数传入;
(2)寻找自己在哪个函数中被定义, 并把该函数的第一个参数拿过来, 作为自己的第二个参数传入.
多继承下的 super
多继承的情况并没有什么不同, 只要搞清楚 mro 序列的情况就能很清楚地知道super到底会调用谁的方法了.
比如:
1class A:
2 def __init__(self):
3 print('A')
4class B:
5 def __init__(self):
6 print('B')
7class C(A,B):
8 def __init__(self):
9 super(C,self).__init__()
10 print('C')
11class D(B,A):
12 def __init__(self):
13 super(B,self).__init__()
14 print('D')
15print(C.__mro__)
16print(D.__mro__)
17print('initi C:')
18c = C()
19print('initi D:')
20d = D()
21
22# (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
23# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
24# initi C:
25# A
26# C
27# initi D:
28# A
29# D
可以看到, super 将访问到对应的类在 实例 mro 序列中的后一个类.
super 调用父类方法
借助 super 可以直接调用父类方法, 不过此时此时父类中 self 并不是父类的实例而是子类的实例, 下面的代码能够较好的说明这个问题:
1class A:
2 def __init__(self):
3 self.n = 2
4
5 def add(self, m):
6 print('self is {0} @A.add'.format(self))
7 self.n += m
8
9
10class B(A):
11 def __init__(self):
12 self.n = 3
13
14 def add(self, m):
15 print('self is {0} @B.add'.format(self))
16 super().add(m)
17 self.n += 3
18
19b = B()
20b.add(2)
21print(b.n)
22
23# self is <__main__.B object at 0x106c49b38> @B.add
24# self is <__main__.B object at 0x106c49b38> @A.add
25# 8
参考:
-
Python多重继承super()的MRO坑 · 零壹軒·笔记; 明白了 mro, 多继承, super 后再读这篇文章就豁然开朗了
类的创建过程
1class A:
2 name = "AAA"
3 def f(self):
4 print("f")
5
6print(A.__dict__)
7
8# {'__module__': '__main__', 'name': 'AAA', 'f': <function A.f at 0x105b340d0>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
以该类为例, 当解释器遇到class A的定义代码后, 将会运行 class 内部所有的代码, 随后将产生的所有局部变量的内容存在__dict__
字典下; 再调用内置函数 __build_class__
, 它返回一个 type类型的对象, 然后把这个 type 的对象保存到类名这个变量里, 这就是一个典型的静态创建类的过程.
至此也可以发现, type
其实是一个元类, 即创建类的类, 所有的类都是 type 的实例.
那么就自然可以给出一种动态创建类的方法:
1def f(self):
2 print("f")
3dic = {
4 "name": "AAA",
5 "f": f
6}
7
8A = type('A', (), dic)
type(x1, x2, x3)
返回一个 type 的实例: 其中第一个参数是类的名字; 第二个参数是这个类的父类; 第三个参数一个字典, 规定类的属性和方法.
在一些场景下类的某些属性依赖于动态参数, 就可以使用这种基于type的动态创建类的方式.
type 既可以查看 object 的类型(1个参数), 又可以创建类(3个参数), 这是很令人困惑, 也容易出错的; 这大概是 Python 的历史包袱吧.
魔术方法与类的实例化
在类的实例化过程中, 主要关注__new__
, __init__
以及__call__
这三个魔术方法.
在一个类的实例化过程中, 如Foo(*args, **kwargs)
:
-
对
Foo
类的调用等价于调用Foo.__call__(*args, **kwargs)
; 不过 Foo 类本身未必实现了__call__
方法, 因而其将使用其父类type
的方法, 而type.__call__
方法是使用C实现的, 其大致等价于:1def __call__(obj_type, *args, **kwargs): 2 obj = obj_type.__new__(*args, **kwargs) 3 if obj is not None and issubclass(obj, obj_type): 4 obj.__init__(*args, **kwargs) 5 return obj
-
type.__call__
将进一步调用type.__new__(Foo, *args, **kwargs)
, 返回Foo
类型的实例. -
调用
obj
的实例方法__init__
, 对实例进行初始化 -
返回实例
obj
当需要重写 __new__
方法时, 因为当前的__new__
已经被覆盖了, 需要借助 super.__new__
方法来构造实例:
1class Example1(object):
2 def __new__(cls):
3 return super(Example1, cls).__new__(cls)
4
5
6class Example2(object):
7 def __new__(cls):
8 pass
9
10
11print('For Example1: %s' % Example1())
12print('For Example2: %s' % Example2())
13
14# For Example1: <__main__.Example1 object at 0x021B36B0>
15# For Example2: None
元类的重写
type
本身也是一个类, 那么也可以继承他:
1class M(type):
2 def __new__(cls, name, bases, dict):
3 return type.__new__(cls, name, bases, dict)
4
5 def __init__(self, name, bases, dict):
6 return type.__init__(self, name, bases, dict)
7
8 def __call__(cls, *args, **kwds):
9 return type.__call__(cls, *args, **kwds)
所有继承了 type
类的都是元类, 使用自定义的元类可以控制其他类的生成方式; 比如:
1# 静态方法
2class A(metaclass=M):
3 ...
4
5# 动态方法
6A = M('A', (), dic)
在类的定义中指定metaclass就可以要求解释器按照指定的元类的规则去生成类; 或者可以直接通过动态实例化的方式直接生成对应的元类的实例.
通过自定义元类的魔术方法, 可以在不同的阶段做一些额外的事情:
-
自定义 __new__: 在类建立的过程中动些手脚;
-
自定义 __init__: 在对类做初始化的时候动些手脚, 比如给这个类绑定一些属性, 即使在类的定义中没有这些属性;
-
自定义 __call__: 对这个类做实例化的时候动些手脚, 典型应用是实现单例 (single instance).
在元类中实现单例吗? 应该是在类的__call__中实现单例?
参考: