只在用编写Max-in组件的工具类的时候使用多继承

Python是一个面向对象的可以通过内置模块来使得多继承稳固工作的的编程语言(详见第25项:使用super函数初始化父类)。然而,避免一下继承多个父类是我们更推崇的一种方式。

如果你发现自己渴望随继承的便利和封装,那么考虑mix-in吧。它是一个只定义了几个类必备功能方法的很小的类。Mix-in类不定义以自己的实例属性,也不需要它们的初始化方法__init__被调用。

编写一个mix-in类是很容易的一件事,因为Python可以无视对象的类型而直接感知类内部的当前状态。动态的检查可以方便的让你编写通用的功能,一次编写,多次运行。Mix-in可以被分层和组织成最小化的代码块,方便代码的重用。例如:你想从内存中的一个准备好序列化了的对象转成一个字典。为什么不写一个通用的代码呢?这样你就可以将其运用到所有的类中了。这里,我定义了一个简单的mix-in类,附带一个新的公共方法(只要继承这个类,子类就也会拥有这个方法了)。

class ToDictMixin(object):
    """实现的细节是直截了当的。通过hasattr方法动态获取属性,动态探测实例对象的类型,访问对象的字典事例__dict__。

    """

    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 instance(value, ToDictMixin):
           return value.to_dict()
       elif instance(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

这里,我定义了一个例子:使用mix-in来制作一个代表二叉树的字典。

class BinaryTree(ToDIctMixin):


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


# 这下把大量的Python对象转换到一个字典中变得容易多了。
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的最好的部分就是你可以使用它们的通用的可插拔式的功能,所以当有必要的时候你还可以重写。例如:这里我定义了一个BinaryTree的子类来保存其父类的一个引用,这样的环状引用就可以导致ToDictMixin.to_dict的死循环。这是极其不好的,所以我们要尽量的去规避这个问题,如下:

class BinaryTreeWithParent(BinaryTree):


    def __init__(self, value, parent=None, left=None, right=None):
        super().__init__(value, left=left, right=right)
        self.parent = parent

    """解决办法就是在子类中重写_traverse方法来仅仅处理相关的问题,阻止mix-in的环路出现。
    这里,我重写了_traverse方法,不让它遍历父类,而仅仅是插入一个它的数字代表值。
    """

    def _traverse(self, key, value):
        if (isinstance(value, BinaryTreeWithParent) and key == 'parent'):
            return value.value #防止循环
        else:
            return super()._traverse(key, value)

#调用BinaryTreeWithParent.to_dict将会没有问题的良好工作了,这正是没有了环路问题的体现。
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_eith_parent):
        self.name = name
        self.tree_with_parent = tree_with_parent


my_tree = NamedSubTree('foobar', root.left.right)
print(my_tree.to_dict())  # 不会导致循环
>>>
{'name': 'foobar',
 'tree_with_parent': {'left': None,
                    'parent': 7,
                    'right': None,
                    'value': 9}
}

Min-in也可以被组件化。例如,你想使用一个mix-in来为一些类提供对JSON的序列化功能。你可以通过假设这个类提供了to_dict方法(可能存在于ToDictMixin中,也可能不存在)。

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是怎么及定义了实例方法,又定义了类方法的。Min-in可以让你添加任意的行为。在这个例子中,唯一的必须条件就是类中必须有一个to_dict方法和接收关键字参数的__init__构造方法(详见第19项:使用关键字参数来提供可选则的行为)。 这种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):
    # ···

class Machine(ToDictMixin, JsonMixin):
    # ···


# 将这些类从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()
assert json.loads(serialized) == json.loads(roundtrip)

当你像这样使用Mix-in的时候,它还可以服务于继承自JsonMixin或者更高层的对象。结果类将会表现的很不错哦。


备忘录

  • 如果可以使用mix-in实现相同的结果输出的话,就不要使用多继承了。
  • mix-in类需要的时候,在实例级别上使用可插拔的行为可以为每一个自定义的类工作的更好。
  • 从简单的行为出发,创建功能更为灵活的mix-in

results matching ""

    No results matching ""