第三章:类与继承(Ⅳ)
这篇文章是基于 《Effective Python——编写高质量Python代码的59个有效方法》[美] 布雷特·斯拉特金 著 爱飞翔 译 这本书中的内容,写写自己在某方面的感悟,并摘录一些作为读书笔记供今后鞭策。侵删。
继承 collections.abc 以实现自定义的容器类型
大部分的 Python 编程工作,其实都是在定义类。类可以包含数据,并且能够描述出这些数据对象之间的交互方式。Python 中的每一个类,从某种程度上来说都是容器,它们都封装了属性与功能。Python 也直接提供了一些管理数据所用的内置容器类型,例如,list、tuple、set、dictionary等。
如果要设计用法比较简单的序列,那我们自然就会想到直接继承 Python 内置的 list 类型。例如,要创建一种自定义的类型,并提供统计各种元素出现的方法。
In [1]: class FrequencyList(list):
...: def __init__(self, members):
...: super().__init__(members)
...: def frequency(self):
...: counts = {}
...: for item in self:
...: counts.setdefault(item, 0)
...: counts[item] += 1
...: return counts
...:
上面这个 FrequencyList 类继承了 list, 并获得了由 list 所提供的全部标准功能,使得所有 Python 程序员都可以用他们熟悉的写法来使用这个类。此外,我们还根据自己的需求,在子类里添加了其他的方法,以定制其行为。
In [2]: foo = FrequencyList(['a', 'b', 'a', 'c', 'b', 'a'])
...:
In [3]: len(foo)
Out[3]: 6
In [4]: foo.pop()
Out[4]: 'a'
In [5]: repr(foo)
Out[5]: "['a', 'b', 'a', 'c', 'b']"
In [6]: foo.frequency()
Out[6]: {'a': 2, 'b': 2, 'c': 1}
现在,假设要编写这么一种对象:它本身虽然不属于 list 子类,但是用起来却和 list 一样,也可以通过下标访问其中的元素。例如:我们要令下面这个表示二叉树节点的类,也能够像 list 或 tuple 等序列来访问。
In [9]: class BinaryNode(object):
...: def __init__(self, value, left=None, right=None
...: ):
...: self.value=value
...: self.left=left
...: self.right=right
...:
上面这个类,如何才能够表现得和序列类型一样呢?我们可以通过特殊方法完成此功能。Python 会用一些名称比较特殊的实例方法,来实现与容器有关的行为。用下标访问序列中的元素时:
bar = [1,2,3]
bar[0]
Python 会把访问代码转译为:
bar.__getitem__(0)
于是,我们提供自己定制的__getitem __ 方法,令 BinaryNode 类可以表现得和序列一样。下面这个方法按深度优先的次序来访问二叉树中的对象。
In [10]: class IndexableNode(BinaryNode):
...: def _search(self, count, index):
...: # ...
...: # Returns(found, count)
...: pass
...: def __getitem__(self, index):
...: found, _ = self._search(0, index)
...: if not found:
...: raise IndexError('Index out of range')
...:
...: return found.value
...:
构建二叉树的代码,依然与平常一样。
In [11]: tree = IndexableNode(10, left=IndexableNode(5, lef
...: t=IndexableNode(2), right=IndexableNode(6, right=I
...: ndexableNode(7))), right=IndexableNode(15, left=In
...: dexableNode(11)))
但是访问它的时候,除了可以像普通的二叉树那样进行遍历外,还可以使用与 list 相同的写法来访问树中的元素。
In [12]: print(tree.left.right.right.value)
7
然而只实现 __getitem __ 方法是不够的,它并不能使该类型支持我们想要的每一种序列操作。
In [15]: len(tree)
------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-15-dc0343ec22f7> in <module>
----> 1 len(tree)
TypeError: object of type 'IndexableNode' has no len()
想要使内置的 len 函数正常运作,就必须在自己定制的序列类型中实现另外一个名叫__len __ 的方法。
class SequenceNode(IndexableNode):
def __len__(self):
_, count = self._search(0, None)
return count
tree = SequenceNode(
...)
实现了 __len __方法后,这个类的功能依然不太完善,其他 Python 程序员还希望这个序列能够像 list 或 tuple 那样,提供 count 和 index 方法。这样看来,定义自己的容器类型,似乎要比想象中困难得多。
为了在编写 Python 程序时避免这些麻烦,我们可以使用内置的 collections.abc 模块。该模块定义了一系列抽象基类,它们提供了每一种容器所应具备的常用方法。从这样的基类中继承了子类之后,如果忘记实现某个方法,那么 collections.abc 模块就会指出这个错误。
对于 Set 和 MutableMapping 等更为复杂的容器类型来说,若不继承抽象基类,则必须实现非常多的特殊方法,才能令自己所定制的子类符合 Python 编程习惯。这种情况下,继承抽象类所带来的好处会更加明显。
memo
- 如果要定制的子类比较简单,那就可以直接从 Python 的容器类型中继承。
- 想正确实现自定义的容器类型,可能需要编写大量的特殊方法。
- 编写自制的容器类型时,可以从 collections.abc 模块的抽象基类中继承,那些基类能够确保我们的子类具备适当的接口以及行为。
上一篇: canvas绘制图片,每日打卡卡片就是这样生成的!
下一篇: 第三章继承与多态3.2.4