Effective Python 读书笔记 - 第26条:只在使用 Mix-in组件制作工具类时进行多重继承

Python是面向对象的编程语言,它提供了多重继承的代码复用机制。但是,我们应该尽量避免使用多重继承。

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

由于Python程序可以方便地查看各类对象的当前状态,所以编写mix-in比较容易。我们可以在mix-in类里面通过动态检测机制先编写一套通用的功能代码,稍后再利用继承机制将其应用到其他很多类上面。

例如,要把内存中的Python对象转换为字典形式,以便将其序列化。下列代码定义了实现该功能所用的mix-in类,在其中添加了一个public方法,其他任何类可以通过继承这个mix-in类来具备此功能:

class ToDictMixin:
    def to_dict(self):
        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):
        if isinstance(value, ToDictMixin): # 如果value是一个支持to_dict方法的对象(TodictMixin的子类)
            return value.to_dict()
        elif isinstance(value, dict): # 否则如果value是一个字典对象
            return self._traverse_dict(value)
        elif isinstance(value, list): # 否则如果value是一个列表对象
            return [self._traverse(key, i) for i in value]
        elif hasattr(value, '__dict__'): # 否则如果value是一个具有__dict__属性的对象
            return self._traverse_dict(value.__dict__)
        else:
            return value

下面演示如何用mix-in把二叉树表示为字典:

class BinaryTree(ToDictMixin):
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right


tree = BinaryTree(10,
    left=BinaryTree(7, right=BinaryTree(9)),
    right=BinaryTree(13, left=BinaryTree(11)))

print(tree.to_dict())

>>>
{'left': {'left': None, 
          'right': {'left': None, 'right': None, 'value': 9}, 
          'value': 7}, 
'right': {'left': {'left': None, 'right': None, 'value': 11}, 
          'right': None, 
          'value': 13}, 
'value': 10}

mix-in的最大优势在于,你可以把通用的功能做成插件,并在需要时覆写这些行为。例如我定义了一个二叉树的子类,每个节点存放着它的父节点的引用。假如采用默认的ToDictMixin.to_dict()来处理它,程序会因为循环引用而陷入死循环。

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的实现代码不会陷入死循环。下面覆写的这个_traverse()方法,不再遍历parent节点,而只是把parent节点对应的value插入到最终生成的字典里面。

def _traverse(self, key, value):
    if isinstance(value, BinaryTreeWithParent) and
            key == 'parent'):
        return value.value    # Prevent cycles
    else:
        return super()._traverse(key, value)

现在调用BinaryTreeWithParent.to_dict()是不会有问题的,因为程序已经不再追踪导致循环引用的那个parent属性了。

root = BinaryTreeWithParent(10)
root.left = BinaryTreeWithParent(7, parent=root)
root.left.right = BinaryTreeWithParent(9, parent=root.left)
print(root.to_dict())

>>>
{'left': {'left': None,
          'parent': 10,
          'right': {'left': None,
                    'parent': 7,
                    'right': None,
                    'value': 9},
          'value': 7},
'parent': None,
'right': None,
'value': 10}

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

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)
print(my_tree.to_dict))    # No infinite loop

>>>
{'name': 'foobar',
'tree_with_parent': {'left': None,
                     'parent': 7,
                     'right': None,
                     'value': 9}}

 

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

class JsonMixin(object):
    @classmethod
    def from_json(cls, data):
        kwargs = json.loads(data)    # 把JSON格式的字符串转换为Python的字典对象
        return cls(**kwargs)

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

JsonMixin既定义了类方法又定义了实例方法。Mixin能让你添加任何一种行为。在这个例子中,JsonMixin的唯一要求是这个子类有to_dict实例方法,并且它的__init__方法接受关键字参数。

有了这样的mix-in之后,我们只需编写极少量的代码,就可以通过继承体系,轻松创建出相关的工具类,以便实现序列化数据以及从JSON中读取数据的功能。例如,如下这些类代表数据中心的拓扑结构:

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, ports=None, speed=None):
        self.ports = ports
        self.speed = speed
        
class Machine(ToDictMixin, JsonMixin):
    def __init__(self, cores=None, ram=None, disk=None):
        self.cores = cores
        self.ram = ram
        self.disk = disk

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

serialized = """{
    "switch": {"ports": 5, "speed": 1e9},
    "machines": [
        {"cores": 8, "ram": 32e9, "disk": 5e12},
        {"cores": 4, "ram": 16e9, "disk": 1e12},
        {"cores": 2, "ram": 4e9, "disk": 500e9}
    ]
}"""

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

>>>
{"switch": {"ports": 5, "speed": 1000000000.0}, "machines": [{"cores": 8, "ram": 32000000000.0, "disk": 5000000000000.0}, {"cores": 4, "ram": 16000000000.0, "disk": 1000000000000.0}, {"cores": 2, "ram": 4000000000.0, "disk": 500000000000.0}]}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值