只在使用 Mix-in 组件制作工具类时进行多重继承

Python是面向对象的编程语言,它提供了一些内置的编程机制,使得开发者可以适当地实现多重继承。但是我们仍然应该尽量避开多重继承。

前言



若一定要利用多重继承所带来的便利及封装性,那就编写mix-in类。mix-in是一种小型的类,它只定义了其他类可能需要提供的一套附加方法,而不定义自己的实例属性,此外,它也不要求使用者调用自己的__init__构造器。

例子1:ToDictMixin

现在,要把内存中的Python对象转换成字典形式,以便将其序列化,那我们就不妨把这个功能写成通用的代码,以便其他类使用。

ToDictMixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ToDictMixin类
class ToDictMixin(object):
def to_dict(self):
# 用__dict__来访问实例内部的字典
return self._traverse_dict(self.__dict__)

def _traverse_dict(self, instance_dict):
output = {}
for key, value in instance_dict.items():
output[key] = self._traverse(key, value)
return output

def _traverse(self, key, value):
# 根据value不同类型分别作处理
if isinstance(value, ToDictMixin):
return value.to_dict()
elif isinstance(value, dict):
return self._traverse_dict(value)
elif isinstance(value, list):
return [self._traverse(key, i) for i in value]
elif hasattr(value, '__dict__'):
return self._traverse_dict(value.__dict__)
else:
return value

BinaryTree

使用ToDictMixin把二叉树表示为字典:

1
2
3
4
5
6
# 二叉树类
class BinaryTree(ToDictMixin):
def __init__(self, value, left=None, right=None):
self.value= value
self.left = left
self.right = right

现在,我们可以把一大批互相关联的Python对象都轻松地转换成字典:

1
2
3
4
5
6
tree = BinaryTree(1,
left=BinaryTree(2, right=BinaryTree(3)),
right=BinaryTree(4, left=BinaryTree(5)))
print(tree.to_dict())
>>>
{'value': 1, 'right': {'value': 4, 'right': None, 'left': {'value': 5, 'right': None, 'left': None}}, 'left': {'value': 2, 'right': {'value': 3, 'right': None, 'left': None}, 'left': None}}

BinaryTreeWithParent

mix-in的最大优势在于,使用者可以随时安插这些通用的功能,并能在必要的时候覆写它们。

下面定义的这个BinaryTree子类,会持有指向父节点的引用。如果采用默认的ToDictMixin.to_dict来处理它,那么程序就会因为循环引用而陷入死循环(parent)。

1
2
3
4
class BinaryTreeWithParent(BinaryTree):
def __init__(self, value, left=None, right=None, parent=None):
super().__init__(value, left=left, right=right)
self.parent = parent

解决办法是在BinaryTreeWithParent里覆写ToDictMixin._traverse方法,令该方法只处理与序列化有关的值,从而使mix-in的实现代码不会陷入死循环:

1
2
3
4
5
6
7
8
9
10
11
# 覆写_traverse方法,不再遍历父节点,而是只把父节点所对应的数值插入到最终生成的字典里面
class BinaryTreeWithParent(BinaryTree):
def __init__(self, value, left=None, right=None, parent=None):
super().__init__(value, left=left, right=right)
self.parent = parent

def _traverse(self, key, value):
if (isinstance(value, BinaryTreeWithParent) and key == 'parent'):
return value.value # 返回父节点(parent)的值
else:
return super()._traverse(key, value)

调用BinaryTreeWithParent.to_dict看看:

1
2
3
4
5
6
root = BinaryTreeWithParent(1)
root.left = BinaryTreeWithParent(2, parent=root)
root.left.right = BinaryTreeWithParent(4, parent=root.left)
print(root.to_dict())
>>>
{'value': 1, 'right': None, 'left': {'value': 2, 'right': {'value': 4, 'right': None, 'left': None, 'parent': 2}, 'left': None, 'parent': 1}, 'parent': None}

定义了BinaryTreeWithParent._traverse方法之后,如果其他类的某个属性也是BinaryTreeWithParent类型,那么ToDictMixin会自动处理好这些属性:

1
2
3
4
5
6
7
8
9
class NamedSubTree(ToDictMixin):
def __init__(self, name, tree_with_parent):
self.name = name
self.tree_with_parent = tree_with_parent

my_tree = NamedSubTree('foobar', root.left.right) # 上面定义的root
print(my_tree.to_dict())
>>>
{'name': 'foobar', 'tree_with_parent': {'value': 4, 'right': None, 'left': None, 'parent': 2}}

多个mix-in组合

JsonMixin

多个mix-in之间也可以相互组合。例如,可以编写这样一个mix-in,它能够为任意类提供通用的JSON序列化功能。我们可以假定:继承了mix-in的哪个类,会提供名为to_dict的方法(此方法有可能是那个类通过多重继承ToDictMixin而具备的,也有可能不是)。

JsonMixin
1
2
3
4
5
6
7
8
9
# import json first
class JsonMixin(object):
@classmethod
def from_json(cls, data):
kwargs = json.loads(data)
return cls(**kwargs)

def to_json(self):
return json.dumps(self.to_dict())

请注意,JsonMixin类既定义了实例方法,有定义了类方法。这两种行为都可以通过mix-in来提供。在本例中,凡是想继承JsonMixin的类,只需符合两个条件即可:

  • (1) 包含名为to_dict的方法
  • (2) init方法接受关键字参数

组合ToDictMixin和JsonMixin

我们用下面这个继承了mix-in组件的数据类来表示数据中心的拓扑结构:

DatacenterRack
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DatacenterRack(ToDictMixin, JsonMixin):
def __init__(self, switch=None, machines=None):
self.switch = Switch(**switch)
self.machines = [Machine(**kwargs) for kwargs in machines]

class Switch(ToDictMixin, JsonMixin):
def __init__(self, **kwargs):
super().__init__()
# 接受处理关键字参数
for k, w in kwargs.items():
setattr(self, k, w)

class Machine(ToDictMixin, JsonMixin):
def __init__(self, **kwargs):
super().__init__()
# 接受处理关键字参数
for k, w in kwargs.items():
setattr(self, k, w)

对这样的类进行序列化,以及从JSON中加载它,都是比较简单的。下面的这段代码,会重复执行序列化及反序列化操作,以验证这两个功能有没有正确地实现出来。

1
2
3
4
5
6
7
8
9
10
11
12
serialized = """{
"switch": {"ports": 5,"speed":1e9},
"machines": [
{"cores": 8, "ram": 32e9, "disk": 5e12},
{"cores": 4, "ram": 16e9, "disk": 5e12},
{"cores": 2, "ram": 4e9, "disk": 500e9}
]
}"""

deserialized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()
assert json.loads(serialized) == json.loads(roundtrip)

使用这种mix-in的时候,既可以像本例这样,直接继承多个mix-in组件,也可以先令继承体系中的其他类继承相关的mix-in组件,然后再令本类继承那些类,以达到同样的效果

小结

  • 能用mix-in组件实现的效果,就不要用多重继承来做
  • 将各功能实现为可插拔的mix-in组件,然后令相关的类继承自己需要的那些组件,即可定制该类实例所应具备的行为。
  • 把简单的行为封装到mix-in组件里,然后就可以用多个mix-in组合出复杂的行为了。
-------------阅读完毕吐槽一番吧~-------------
0%