分布式系统,程序语言,算法设计

Python 混入类 Mix-Ins

python-learn.png

引言

某次在用到 Python 的 socketserver 时,看到了 ForkingMixInThreadingMixIn。当时就对这种插件式语法糖感觉很神奇。最近自己写代码,也想写一些这种即插即用的插件代码,于是对 python 的 mix-in 机制探究了一番。

简单来说它是利用多继承的特性,通过插拔额外代码片段,对原类进行花样式增强的一种技术。

作者:青藤木鸟 https://www.qtmuniao.com, 转载请注明出处

要点

开宗明义,先说结论,使用MixIn,总结起来只需注意几个要点:

  1. Mix-in class 本质上是代码片段,不能独立存活。
  2. 增强“兄弟” [1] 类中的同名函数功能,来达到可插拔的效果。
  3. 定义子类时,基类继承顺序不能乱,MixIn 类须在被混入类前面。

代码片段

与静态语言不同,Python 是动态binding。因此在定义时,是可以作为代码片段独立“存在”(但并不能“存活”,即不能用其来定义类的实例,进行binding),并且可以引用该片段中并不存在的函数:

1
2
3
4
5
6
7
8
9
10
class SetOnceMappingMixin:
'''
Only allow a key to be set once.
'''
__slots__ = ()

def __setitem__(self, key, value):
if key in self:
raise KeyError(str(key) + ' already set')
return super().__setitem__(key, value)

该代码片段显然不能单独进行实例化,但是单独定义并无妨,而且它假设被混入类具有__setitem__

多继承替换

既然是可插拔,那么便是有没有该Mixin,被混入类[2] API 保持不变。而实现这一机制的原理,便是使用同名函数来替换原函数。而Mixin强调插件作用,即在原有函数实现上,增加额外功能。为了达到这一目的,须复用原函数。而Python中具有该功效的函数,便是super()

在只允许单继承的编程语言中,如Java,super() 毫无争议的是获取父类的引用。但是对于支持多继承的 Python 来说,super() 最终指向谁,就需要安排安排了,看以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base:
def __init__(self):
print('Base.__init__')

class A(Base):
def __init__(self):
super().__init__() # 1
print('A.__init__')

class B(Base):
def __init__(self):
super().__init__() # 2
print('B.__init__')

class C(A,B):
def __init__(self):
super().__init__()
print('C.__init__')

if __name__ == '__main__':
c = C()

可以猜猜输出是什么,然后注释掉 #1 和 #2 看下输出什么。

原因在于,对于每个类,Python会计算出一个方法解析顺序(MRO)列表。通过super().func() 函数会沿着该列表从前往后遍历,找到第一个具有 func 函数的类,然后调用该函数。MRO列表的构建规则很简单,不严谨的说就是从左往右,从下到上

因此类 C的 MRO 列表为[3]:

1
2
3
4
>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
<class '__main__.Base'>, <class 'object'>)
>>>

但如果类继承搞的极度混乱,比如说如果出现环形依赖,在获取 C.__mro__ 时就会报错: TypeError: Cannot create a consistent method resolution ,这里不详细展开,感兴趣的朋友可以自行去 Google。

回到混入类上来,这样依赖,在定义子类时候,混入类为什么要定义在被混入类的前面,也就清楚了:为了使得混入类可以通过 super() 方法调用被混入类的同名函数。

一个完整的例子[4]如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SetOnceMappingMixin:
'''
Only allow a key to be set once.
'''
__slots__ = ()

def __setitem__(self, key, value):
if key in self:
raise KeyError(str(key) + ' already set')
return super().__setitem__(key, value)

class SetOnceDefaultDict(SetOnceMappingMixin, defaultdict):
pass


d = SetOnceDefaultDict(list)
d['x'].append(2)
d['x'].append(3)
# d['x'] = 23 # KeyError: 'x already set'

注解

[1] 这里的 “兄弟”关系指的是多继承中的两个类的关系。比如 class Child(A, B):pass 中类 AB 就是兄弟关系。

[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