十、序列的修改、散列和切片
不要检查它是不是鸭子、它的叫声像不像鸭子、它的走路姿势像不像鸭子,等等。具体检查什么取决于你想使用语言的哪些行为。(comp.lang.python,2000 年 7月 26 日) ——Alex Martelli
多维向量
In [20]: from array import array
In [21]: import reprlib
In [22]: import math
In [29]: class Vector:
...: typecode = 'd'
...: def __init__(self, components):
...: self._components = array(self.typecode, components)
...: def __iter__(self):
...: return iter(self._components)
...: def __repr__(self):
...: components = reprlib.repr(self._components)
...: components = components[components.find('['):-1]
...: return 'Vector({})'.format(components)
...: def __str__(self):
...: return str(tuple(self))
...: def __bytes(self):
...: return (bytes([ord(self.typecode)]) + bytes(self._components))
...: def __eq__(self,other):
...: return tuple(self) == tuple(other)
...: def __abs__(self):
...: return math.sqrt(sum(x*x for x in self))
...: def __bool__(self):
...: return bool(abs(self))
...: @classmethod
...: def frombytes(cls, octets):
...: typecode = chr(octets[0])
...: memv = memoryview(octets[1:]).cast(typecode)
...: return cls(memv)
...: def __len__(self):
...: return len(self._components)
...: def __getitem__(self, index): # 支持 [] 和 切片
...: return self._components[index]
我使用 reprlib.repr 的方式需要做些说明。这个函数用于生成大型结构或递归结构的安全表示形式,它会限制输出字符串的长度,用 '...' 表示截断的部分。我希望 Vector实例的表示形式是 Vector([3.0, 4.0, 5.0]) 这样,而不是 Vector(array('d',[3.0, 4.0, 5.0])),因为 Vector 实例中的数组是实现细节。
编写 __repr__
方法时,本可以使用这个表达式生成简化的 components 显示形式:reprlib.repr(list(self._components))。然而,这么做有点浪费,因为要把self._components 中的每个元素复制到一个列表中,然后使用列表的表示形式。而是直接把 self._components 传给 reprlib.repr 函数,然后去掉 [] 外面的字符。
调用 repr() 函数的目的是调试,因此绝对不能抛出异常。如果 __repr__
方法的实现有问题,那么必须处理,尽量输出有用的内容,让用户能够识别目标对象。
协议和鸭子类型
在面向对象编程中,协议是非正式的接口,只有在文档中定义,在代码中不定义。任何类只要使用标准的签名和语义实现了协议的方法,就能用在任何期待实现协议功能的地方。其是不是哪个类的子类无关紧要,只要提供了所需的方法即可。
比如:序列协议:只要实现 __len__
和 __getitem__
两个方法。
我们说它是序列,因为它的行为像序列,这才是重点。
协议是非正式的,没有强制力,因此如果你知道类的具体使用场景,通常只需要实现一个协议的部分。例如,为了支持迭代,只需要实现 __getitem__
方法,没必要实现 __len__
方法。
支持切片
如果得到的 Vector 对象的切片是一个列表,会丢失很多功能,如果得到是还是一个 vector 对象,就很好了。
切片原理
In [1]: class Y:
...: def __getitem__(self,index):
...: return index
...:
In [2]: y = Y()
In [3]: y[1]
Out[3]: 1
In [4]: y[1:2]
Out[4]: slice(1, 2, None)
In [5]: y[1:2:2,9] # 逗号隔开,index 是元组
Out[5]: (slice(1, 2, 2), 9)
In [6]: y[1:2,2:3] # 支持多个切片
Out[6]: (slice(1, 2, None), slice(2, 3, None))
indices
方法
indices 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法会“整顿”元组,把 start、stop 和 stride 都变成非负数,而且都落在指定长度序列的边界内。
In [11]: slice(None,11,2).indices(5)
Out[11]: (0, 5, 2)
In [12]: slice(None,None,None).indices(10)
Out[12]: (0, 10, 1)
dir() 和 help(),发现 slice.indices() 方法。这也表明交互式控制台是个有价值的工具,能发现新事物。
In [10]: help(slice.indices)
Help on method_descriptor:
indices(...)
S.indices(len) -> (start, stop, stride)
Assuming a sequence of length len, calculate the start and stop
indices, and the stride length of the extended slice described by
S. Out of bounds indices are clipped in a manner consistent with the
handling of normal slices.
__getitem__
处理切片
def __getitem__(self, index):
cls = type(self) # 获取类以构造对象
if isinstance(index, slice):
return cls(self._components[index]) # 返回对象
elif isinstance(index, numbers.Integral):
return self._components[index]
else:
msg = '{cls.__name__} indices must be integers'
raise TypeError(msg.format(cls=cls))
__getattr__
: obj.x
前四个位置的元素分别为:x, y, z, t
shortcut_names = 'xyzt'
def __getattr__(self, name):
cls = type(self)
if len(name) == 1:
pos = cls.shortcut_names.find(name)
if 0 <= pos < len(self._components):
return self._components[pos]
msg = '{.__name__!r} object has no attribute {!r}'
raise AttributeError(msg.format(cls, name))
测试:
>>> v = Vector(range(5))
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
>>> v.x
0.0
>>> v.x = 10 # 赋值给 v 新的 x 属性
>>> v.x # 优先读取 x 属性,而不是直接调用 __getattr__
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
仅当对象没有指定名称的属性时,Python 才会调用那个方法,这是一种后备机制。可是,像 v.x = 10 这样赋值之后,v 对象有 x 属性了,因此使用 v.x 获取 x 属性的值时不会调用 __getattr__
方法了,解释器直接返回绑定到 v.x 上的值,即 10。另一方面,__getattr__
方法的实现没有考虑到 self._components 之外的实例属性,而是从这个属性中获取 shortcut_names 中所列的“虚拟属性”。
实现 __setattr__
方法
def __setattr__(self, name, value):
cls = type(self)
if len(name) == 1:
if name in cls.shortcut_names:
error = 'readonly attribute {attr_name!r}'
elif name.islower(): # 如果 name 是小写字母,为所有小写字母设置一个错误消息。
error = "can't set attributes 'a' to 'z' in {cls_name!r}"
else:
error = ''
if error:
msg = error.format(cls_name=cls.__name__, attr_name=name)
raise AttributeError(msg)
super().__setattr__(name, value) # 默认情况:在超类上调用 __setattr__ 方法,提供标准行为。
为了给 AttributeError 选择错误消息,查看了内置的 complex 类型的行为,因为 complex 对象是不可变的
注意,我们没有禁止为全部属性赋值,只是禁止为单个小写字母属性赋值,以防与只读属性 x、y、z 和 t 混淆。
多数时候,如果实现了 __getattr__
方法,那么也要定义 __setattr__
方法,以防对象的行为不一致。
__setitem__
方法,支持 v[0] = 1.1
可散列对象
累积计算
operator 模块以函数的形式提供了 Python 的全部中缀运算符,从而减少使用 lambda 表达式。
>>> n = 0
>>> for i in range(1, 6):
... n ^= i
>>> import functools
>>> functools.reduce(lambda a, b: a^b, range(6))
>>> import operator
>>> functools.reduce(operator.xor, range(6))
使用 reduce 函数时最好提供第三个参数,reduce(function, iterable, initializer),这样能避免这个异常:TypeError: reduce() of empty sequence with no initial value(这个错误消息很棒,说明了问题,还提供了解决方法)。如果序列为空,initializer 是返回的结果;否则,在归约中使用它作为第一个参数,因此应该使用恒等值。比如,对 +、| 和 ^ 来说, initializer 应该是 0;而对 * 和 & 来说,应该是 1。
from array import array
import reprlib
import math
import functools
import operator
class Vector:
typecode = 'd'
def __eq__(self, other):
return tuple(self) == tuple(other)
def __hash__(self):
hashes = (hash(x) for x in self._components)
return functools.reduce(operator.xor, hashes, 0)
映射过程计算各个分量的散列值,归约过程则使用 xor 运算符聚合所有散列值。把生成器表达式替换成 map 方法,映射过程更明显:
def __hash__(self):
hashes = map(hash, self._components)
return functools.reduce(operator.xor, hashes)
在 Python 2 中使用 map 函数效率低些,因为 map 函数要使用结果构建一个列表。但是在 Python 3 中,map 函数是惰性的,它会创建一个生成器,按需产出结果,因此能节省内存——这与示例 10-12 中使用生成器表达式定义 __hash__
方法的原理一样。
改进 __eq__
方法
def __eq__(self, other):
if len(self) != len(other):
return False
for a, b in zip(self, other): # 由元组构成的生成器
if a != b:
return False
return True
前面比较长度的测试是有必要的,因为一旦有一个输入耗尽,zip 函数会立即停止生成值,而且不发出警告。
改进:上个改进效率很好,不过用于计算聚合值的整个 for 循环可以替换成一行 all 函数调用:如果所有分量对的比较结果都是 True,那么结果就是 True。只要有一次比较的结果是 False,all 函数就返回 False。zip 也是返回生成器。
def __eq__(self, other):
return len(self) == len(other) and all(a == b for a, b in zip(self, other))
出色的 zip 函数
使用 for 循环迭代元素不用处理索引变量,还能避免很多缺陷,但是需要一些特殊的实用函数协助。其中一个是内置的 zip 函数。使用 zip 函数能轻松地并行迭代两个或更多可迭代对象,它返回的元组可以拆包成变量,分别对应各个并行输入中的一个元素。
>>> from itertools import zip_longest # 这个模块可以填充缺失值
>>> list(zip_longest(range(3), 'ABC', [0.0, 1.1, 2.2, 3.3], fillvalue=-1))
[(0, 'A', 0.0), (1, 'B', 1.1), (2, 'C', 2.2), (-1, -1, 3.3)]
输出格式化:__format__
def angle(self, n): # 计算某个角坐标
r = math.sqrt(sum(x * x for x in self[n:]))
a = math.atan2(r, self[n-1])
if (n == len(self) - 1) and (self[-1] < 0):
return math.pi * 2 - a
else:
return a
def angles(self): # 计算全部角坐标
return (self.angle(n) for n in range(1, len(self)))
def __format__(self, fmt_spec=''):
if fmt_spec.endswith('h'): # 超球面坐标
fmt_spec = fmt_spec[:-1]
# 使用 itertools.chain 函数生成生成器表达式,无缝迭代向量的模和各个角坐标。
coords = itertools.chain([abs(self)],self.angles())
outer_fmt = '<{}>'
else:
coords = self
outer_fmt = '({})'
components = (format(c, fmt_spec) for c in coords)
return outer_fmt.format(', '.join(components))
协议,是鸭子类型语言使用的非正式接口。
我们经常分析 Python 标准对象的行为,然后进行模仿,让 Vector 的行为符合 Python 风格。
模仿内置类型实现类时,记住一点:模仿的程度对建模的对象来说合理即可。例如,有些序列可能只需要获取单个元素,而不必提取切片。 ——Python 语言参考手册中“Data Model”一章
不要为了满足过度设计的接口契约和让编译器开心,而去实现不需要的方法,我们要遵守 KISS 原则(http://en.wikipedia.org/wiki/KISS_principle)。
“地道”并不是指使用最鲜为人知 的语言特性。
python 风格的求和方式
我喜欢 Evan Simpson 的代码,不过也喜欢 David Eppstein 对此给出的评论:
如果你想计算列表中各个元素的和,写出的代码应该看起来像是在“计算元素之和”,而不是“迭代元素,维护一个变量 t,再执行一系列求和操作”。如果不能站在一定高度上表明意图,让语言去关注低层操作,那么要高级语言干嘛?
随后,Alex 建议提供并实现了 sum() 函数。这次讨论之后三个月,Python 2.3 就内置了这个函数。因此,Alex 喜欢的句法变成了标准:
>>> sum([sub[1] for sub in my_list])
60
下一年年末(2004 年 11 月),Python 2.4 发布了,这一版引入了生成器表达式。因此,在我看来,Guy Middleton 那个问题目前最符合 Python 风格的答案是:
>>> sum(sub[1] for sub in my_list)
60