• 沒有找到結果。

魔法方法、特性和迭代器

9.3 元素访问

10 11 12 13 14

self.sound = 'Squawk!' def sing(self):

print(self.sound)

这个新式版本与旧式版本等效:

>>> sb = SongBird()

>>> sb.sing() Squawk!

>>> sb.eat() Aaaah ...

>>> sb.eat() No, thanks!

使用函数super有何优点

在我看来,相比于直接对超类调用未关联方法,使用函数super更直观,但这并非其唯一 的优点。实际上,函数super很聪明,因此即便有多个超类,也只需调用函数super一次(条件 是所有超类的构造函数也使用函数super)。另外,对于使用旧式类时处理起来很棘手的问题

(如两个超类从同一个类派生而来),在使用新式类和函数super时将自动得到处理。你无需知

道函数super的内部工作原理,但必须知道的是,使用函数super比调用超类的未关联构造函

数(或其他方法)要好得多。

函数super返回的到底是什么呢?通常,你无需关心这个问题,只管假定它返回你所需的

超类即可。实际上,它返回的是一个super对象,这个对象将负责为你执行方法解析。当你访 问它的属性时,它将在所有的超类(以及超类的超类,等等)中查找,直到找到指定的属性或 引发AttributeError异常。

9.3 元素访问

虽然__init__无疑是你目前遇到的最重要的特殊方法,但还有不少其他的特殊方法,让你能

够完成很多很酷的任务。本节将介绍一组很有用的魔法方法,让你能够创建行为类似于序列或映 射的对象。

基本的序列和映射协议非常简单,但要实现序列和映射的所有功能,需要实现很多魔法方法。

所幸有一些捷径可走,我马上就会介绍。

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

要成为序列,只需遵循序列协议即可。

9.3.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 # 存储步长值

1

def __getitem__(self, key):

"""

从算术序列中获取一个元素 """

check_index(key)

try: return self.changed[key] # 修改过?

except KeyError: # 如果没有修改过,

return self.start + key * self.step # 就计算元素的值 def __setitem__(self, key, value):

"""

修改算术序列中的元素 """

check_index(key)

self.changed[key] = value # 存储修改后的值

>>> s = ArithmeticSequence(1, 2)

>>> 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

if not isinstance(key, int): raise TypeError TypeError

>>> 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.3.2 从 list 、 dictstr 派生

基本的序列/映射协议指定的4个方法能够让你走很远,但序列还有很多其他有用的魔法方法 和普通方法,其中包括将在9.6节介绍的方法__iter__。要实现所有这些方法,不仅工作量大,而 且难度不小。如果只想定制某种操作的行为,就没有理由去重新实现其他所有方法。这就是程序 员的懒惰(也是常识)。

那么该如何做呢?“咒语”就是继承。在能够继承的情况下为何去重新实现呢?在标准库中,

模块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

1

>>> del cl[3:6]

>>> cl

[9, 8, 7, 3, 2, 1, 0]

>>> cl.counter 0

>>> cl[4] + cl[2]