>>> from env_helper import info; info()
页面更新时间: 2022-03-22 23:27:55
运行环境:
    Linux发行版本: Debian GNU/Linux 11 (bullseye)
    操作系统内核: Linux-5.10.0-11-amd64-x86_64-with-glibc2.31
    Python版本: 3.9.2

9.6. 元素访问

虽然 __init__ 无疑是你目前遇到的最重要的特殊方法,但还有不少其他的特殊方法, 让你能够完成很多很酷的任务。 本节将介绍一组很有用的魔法方法,让你能够创建行为类似于序列或映射的对象。

基本的序列和映射协议非常简单,但要实现序列和映射的所有功能,需要实现很多魔法方法。 所幸有一些捷径可走,我马上就会介绍。

注意 在Python中,协议通常指的是规范行为的规则,有点类似于接口。协议指定 应实现哪些方法以及这些方法应做什么。在Python中,多态仅仅基于对象的行为,因此这个概念很重要:其他的语言可能要求对象 属于特定的类或实现了特定的接口,而Python通常只要求对象遵循特定的协议。因此, 要成为序列,只需遵循序列协议即可。

9.6.1. 基本的序列和映射协议

序列和映射基本上是元素(item)的集合,要实现它们的基本行为(协议),不可变对象需 要实现2个方法,而可变对象需要实现4个。

  • __len__(self):这个方法应返回集合包含的项数,对序列来说为元素个数,对映射来说 为键-值对数。如果__len__返回零(且没有实现覆盖这种行为的__nonzero__),对象在布 尔上下文中将被视为假(就像空的列表、元组、字符串和字典一样)。

  • __getitem__(self, key):这个方法应返回与指定键相关联的值。对序列来说,键应该是 0~n - 1的整数(也可以是负数,这将在后面说明),其中n为序列的长度。对映射来说, 键可以是任何类型。

  • __setitem__(self, key, value):这个方法应以与键相关联的方式存储值,以便以后能够 使用__getitem__来获取。当然,仅当对象可变时才需要实现这个方法。

  • __delitem__(self, key):这个方法在对对象的组成部分使用__del__语句时被调用,应 删除与key相关联的值。同样,仅当对象可变(且允许其项被删除)时,才需要实现这个方法。

对于这些方法,还有一些额外的要求。

  • 对于序列,如果键为负整数,应从末尾往前数。换而言之,x[-n]应与x[len(x)-n]等效。

  • 如果键的类型不合适(如对序列使用字符串键),可能引发TypeError异常。

  • 对于序列,如果索引的类型是正确的,但不在允许的范围内,应引发IndexError异常。 要了解更复杂的接口和使用的抽象基类(Sequence),请参阅有关模块collections的文档。 下面来试一试,看看能否创建一个无穷序列。

>>> def check_index(key):
>>>     """
>>>     指定的键是否是可接受的索引?
>>>     键必须是非负整数,才是可接受的。如果不是整数,
>>>     将引发TypeError异常;如果是负数,将引发Index
>>>     Error异常(因为这个序列的长度是无穷的)
>>>     """
>>>     if not isinstance(key, int):
>>>         raise TypeError
>>>     if key < 0:
>>>         raise IndexError
>>> class ArithmeticSequence:
>>>     def __init__(self, start=0, step=1):
>>>         """
>>>         初始化这个算术序列
>>>         start -序列中的第一个值
>>>         step
>>>         -两个相邻值的差
>>>         changed -一个字典,包含用户修改后的值
>>>         """
>>>         self.start = start
>>>         self.step = step
>>>         # 存储起始值
>>>         # 存储步长值9.3 元素访问
>>>         self.changed = {}
>>>         # 没有任何元素被修改
>>> def __getitem__(self, key):
>>>     """
>>>     从算术序列中获取一个元素
>>>     """
>>>     check_index(key)
>>>     try:
>>>         return self.changed[key]
>>>     except KeyError:
>>>         return self.start + key * self.step
>>>     check_index(key)
>>>     self.changed[key] = value
>>> def __setitem__(self, key, value):
>>>     """
>>>     修改算术序列中的元素
>>>     """
>>>     self.changed[key] = value

这些代码实现的是一个算术序列,其中任何两个相邻数字的差都相同。第一个值是由构造函 数的参数start(默认为0)指定的,而相邻值之间的差是由参数step(默认为1)指定的。你允 许用户修改某些元素,这是通过将不符合规则的值保存在字典changed中实现的。如果元素未被 修改,就使用公式self.start + key * self.step来计算它的值。

下面的示例演示了如何使用这个类:

s = ArithmeticSequence(1, 2)
s[4]
>>> s[4] = 2
>>> s[4]
>>> s[5]

请注意,我要禁止删除元素,因此没有实现 del :

>>> del s[4]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: ArithmeticSequence instance has no attribute '__delitem__'

另外,这个类没有方法__len__,因为其长度是无穷的。

如果所使用索引的类型非法,将引发 TypeError 异常;如果索引的类型正确,但不在允许的 范围内(即为负数),将引发 IndexError 异常。

>>> s["four"]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "arithseq.py", line 31, in __getitem__
check_index(key)
File "arithseq.py", line 10, in checkIndex
>>> s[-42]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "arithseq.py", line 31, in __getitem__
check_index(key)
File "arithseq.py", line 11, in checkIndex
if key < 0: raise IndexError
IndexError

索引检查是由我为此编写的辅助函数check_index负责的。

9.6.2. 从 list、dict 和 str 派生

基本的序列/映射协议指定的4个方法能够让你走很远,但序列还有很多其他有用的魔法方法和普通方法。 要实现所有这些方法,不仅工作量大,而 且难度不小。 如果只想定制某种操作的行为,就没有理由去重新实现其他所有方法。

那么该如何做呢?“咒语”就是继承。在能够继承的情况下为何去重新实现呢? 在标准库中, 模块collections提供了抽象和具体的基类,但你也可以继承内置类型。 因此,如果要实现一种 行为类似于内置列表的序列类型,可直接继承list。

来看一个简单的示例——一个带访问计数器的列表。

>>> class CounterList(list):
>>>     def __init__(self, *args):
>>>         super().__init__(*args)
>>>         self.counter = 0
>>>     def __getitem__(self, index):
>>>         self.counter += 1
>>>         return super(CounterList, self).__getitem__(index)

CounterList 类深深地依赖于其超类( list )的行为。 CounterList 没有重写的方法(如 append 、 extend 、 index 等)都可直接使用。在两个被重写的方法中,使用 super 来调用超类的相应方法,并添加了必要的行为:初始化属性 counter (在 __init__ 中)和更新属性 counter (在__getitem__中)。

注意 重写 getitem 并不能保证一定会捕捉用户的访问操作,因为还有其他访问列表内容的方式,如通过方法pop。

下面的示例演示了CounterList的可能用法:

>>> cl = CounterList(range(10))
>>> cl
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> cl.reverse()
>>> cl
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> del cl[3:6]
>>> cl
[9, 8, 7, 3, 2, 1, 0]
>>> cl.counter
4
>>> cl[4] + cl[2]
12
>>> cl.counter
6

如你所见,CounterList的行为在大多数方面都类似于列表,但它有一个counter属性(其初 始值为0)。每当你访问列表元素时,这个属性的值都加1。执行加法运算cl[4] + cl[2]后,counter 的值递增两次,变成了2。

9.6.3. 其他魔法方法

特殊(魔法)名称的用途很多,前面展示的只是冰山一角。魔法方法大多是为非常高级的用途准备的,因此这里不详细介绍。然而,如果你感兴趣,可以模拟数字,让对象像函数一样被调用,影响对象的比较方式,等等。