引言
某次在用到 Python 的 socketserver 时,看到了 ForkingMixIn
和 ThreadingMixIn
。当时就对这种插件式语法糖感觉很神奇。最近自己写代码,也想写一些这种即插即用的插件代码,于是对 python 的 mix-in 机制探究了一番。
简单来说它是利用多继承的特性,通过插拔额外代码片段,对原类进行花样式增强的一种技术。
作者:木鸟杂记 https://www.qtmuniao.com, 转载请注明出处
要点
开宗明义,先说结论,使用MixIn,总结起来只需注意几个要点:
- Mix-in class 本质上是代码片段,不能独立存活。
- 增强“兄弟” [1] 类中的同名函数功能,来达到可插拔的效果。
- 定义子类时,基类继承顺序不能乱,MixIn 类须在被混入类前面。
代码片段
与静态语言不同,Python 是动态binding。因此在定义时,是可以作为代码片段独立“存在”(但并不能“存活”,即不能用其来定义类的实例,进行binding),并且可以引用该片段中并不存在的函数:
1 | class SetOnceMappingMixin: |
该代码片段显然不能单独进行实例化,但是单独定义并无妨,而且它假设被混入类具有__setitem__
。
多继承替换
既然是可插拔,那么便是有没有该Mixin,被混入类[2] API 保持不变。而实现这一机制的原理,便是使用同名函数来替换原函数。而Mixin强调插件作用,即在原有函数实现上,增加额外功能。为了达到这一目的,须复用原函数。而Python中具有该功效的函数,便是super()
。
在只允许单继承的编程语言中,如Java,super() 毫无争议的是获取父类的引用。但是对于支持多继承的 Python 来说,super() 最终指向谁,就需要安排安排了,看以下例子:
1 | class Base: |
可以猜猜输出是什么,然后注释掉 #1 和 #2 看下输出什么。
原因在于,对于每个类,Python会计算出一个方法解析顺序(MRO)列表。通过super().func()
函数会沿着该列表从前往后遍历,找到第一个具有 func
函数的类,然后调用该函数。MRO列表的构建规则很简单,不严谨的说就是从左往右,从下到上。
因此类 C
的 MRO 列表为[3]:
1 | C.__mro__ |
但如果类继承搞的极度混乱,比如说如果出现环形依赖,在获取 C.__mro__
时就会报错: TypeError: Cannot create a consistent method resolution
,这里不详细展开,感兴趣的朋友可以自行去 Google。
回到混入类上来,这样依赖,在定义子类时候,混入类为什么要定义在被混入类的前面,也就清楚了:为了使得混入类可以通过 super()
方法调用被混入类的同名函数。
一个完整的例子[4]如下:
1 | class SetOnceMappingMixin: |
注解
[1] 这里的 “兄弟”关系指的是多继承中的两个类的关系。比如 class Child(A, B):pass
中类 A
和 B
就是兄弟关系。
[2] 被混入类,即Mixin的“兄弟”。如 class Strong(Week, PowerMixIn)
,Week
即是 PowerMixin
的被混入类。
[3] 调用父类方法:https://python3-cookbook.readthedocs.io/zh_CN/latest/c08/p07_calling_method_on_parent_class.html
[4] 利用Mixins扩展类功能: https://python3-cookbook.readthedocs.io/zh_CN/latest/c08/p18_extending_classes_with_mixins.html