虽然 __init__
无疑是目前遇到的最重要的特殊方法,但还有不少其他的特殊方法,
能够完成很多很酷的任务。
本节将介绍一组很有用的魔法方法,能够创建行为类似于序列或映射的对象。
基本的序列和映射协议非常简单,但要实现序列和映射的所有功能,需要实现很多魔法方法。 所幸有一些捷径可走,马上就会介绍。
注意:在Python中,协议通常指的是规范行为的规则,有点类似于接口。协议指定 应实现哪些方法以及这些方法应做什么。在Python中,多态仅仅基于对象的行为,因此这个概念很重要:其他的语言可能要求对象 属于特定的类或实现了特定的接口,而Python通常只要求对象遵循特定的协议。因此, 要成为序列,只需遵循序列协议即可。
序列和映射基本上是元素(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
异常。
下面来试一试,看看能否创建一个无穷序列。
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]
9
s[4] = 2
s[4]
2
s[5]
11
请注意,要禁止删除元素,因此没有实现 del
:
# del s[4] #此代码会报错
另外,这个类没有方法__len__
,因为其长度是无穷的。
如果所使用索引的类型非法,将引发 TypeError
异常;如果索引的类型正确,但不在允许的范围内(即为负数),将引发 IndexError
异常。
# s["four"] #此代码会报错
# s[-42] #此代码会报错
索引检查是为此编写的辅助函数 check_index
负责的。
基本的序列/映射协议指定的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
0
cl[4] + cl[2]
9
cl.counter
2
CounterList
的行为在大多数方面都类似于列表,但它有一个 counter
属性(其初 始值为0)。
每当访问列表元素时,这个属性的值都加1。执行加法运算 cl[4] + cl[2]
后,counter
的值递增两次,
变成了2。