欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

第三章:类与继承(Ⅳ)

程序员文章站 2022-07-16 09:24:49
...

这篇文章是基于 《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 模块的抽象基类中继承,那些基类能够确保我们的子类具备适当的接口以及行为。

第三章:类与继承(Ⅳ)