序列的修改、散列和切片
- 本章将以上一篇博客定义的二维向量 Vector2d 类为基础,向前迈出一大步,定义表示多维向量的 Vector 类。这个类的行为与 Python 中标准的不可变扁平序列一样。Vector 实例中的元素是浮点数,本章结束后Vector 类将支持下述功能:
- 基本的序列协议——
__len__ 和 __getitem__
- 正确表述拥有很多元素的实例
- 适当的切片支持,用于生成新的 Vector 实例
- 综合各个元素的值计算散列值
- 自定义的格式语言扩展
- 基本的序列协议——
- 此外,我们还将通过
__getattr__
方法实现属性的动态存取,以此取代 Vector2d 使用的只读特性——不过,序列类型通常不会这么做。 - 在此期间,我们将穿插讨论一个概念:
把协议当作正式接口
。我们将说明协议和鸭子类型之间的关系,以及对自定义类型的实际影响。 - 我们将使用组合模式实现 Vector 类,而不使用继承。向量的分量存储在浮点数数组中,而且还将实现不可变扁平序列所需的方法。
1. Vector类第1版:与 Vector2d 类兼容
- 先来看上一篇博客中定义的 Vector2d :
from array import array
import math
class Vector2d:
type_code = 'd'
def __init__(self, x, y):
self.__x = float(x)
self.__y = float(y)
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
def __hash__(self):
return hash(self.x) ^ hash(self.y)
def __complex__(self):
return complex(real=self.x, imag=self.y)
def __iter__(self):
return (i for i in (self.x, self.y))
def __repr__(self):
class_name = type(self).__name__
return '{}(!r), {!r}'.format(class_name, *self)
def __str__(self):
return str(tuple(self))
def __bytes__(self):
return bytes([ord(self.type_code)]) + bytes(array(self.type_code, self))
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def angle(self):
return math.atan2(self.y, self.x)
def __format__(self, format_spec=''):
if format_spec.endswith('p'):
format_spec = format_spec[:-1]
coords = (abs(self), self.angle())
out_fmt = '<{}, {}>'
else:
coords = self
out_fmt = '({}, {})'
compontens = (format(c, format_spec) for c in coords)
return out_fmt.format(*compontens)
@classmethod
def frombytes(cls, b):
type_code = chr(b[0])
memv = memoryview(b[1:]).cast(type_code)
return cls(*memv)
- Vector 类的第 1 版要尽量与前一章定义的 Vector2d 类兼容。为了编写 Vector(3, 4) 和 Vector(3, 4, 5) 这样的代码,我们可以让
__init__
方法接受任意个参数(通过 *args);但是,序列类型的构造方法最好接受可迭代的对象为参数,因为所有内置的序列类型都是这样做的。
第一版 Vector 如下所示:
import math
import reprlib
from array import array
class Vector:
type_code = 'd'
def __init__(self, components):
# self._components 是“受保护的”实例属性,把 Vector 的分量保存在一个数组中
self._components = array(self.type_code, components)
def __iter__(self):
# 为了迭代,我们使用 self._components 构建一个迭代器
return iter(self._components)
def __repr__(self):
# 使用 reprlib.repr() 函数获取 self._components 的有限长度表示形式(如 array('d', [0.0, 1.0, 2.0, 3.0, 4.0,...]))。
components = reprlib.repr(self._components)
# 把字符串插入 Vector 的构造方法调用之前,去掉前面的array('d' 和后面的 )。
components = components[components.find('['):-1]
return 'Vector({})'.format(components)
def __str__(self):
return str(tuple(self))
def __bytes__(self):
# 直接使用 self._components 构建 bytes 对象。
return bytes([ord(self.type_code)]) + bytes(self._components)
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
# 不能使用 hypot(平方根) 方法了,因此我们先计算各分量的平方之和,然后再使用 sqrt 方法开平方。
return math.sqrt(sum(x * x for x in self))
def __bool__(self):
return bool(abs(self))
@classmethod
def frombytes(cls, b):
type_code = chr(b[0])
memv = memoryview(b[1:]).cast(type_code)
# 因为直接传入序列(components),把 memoryview 传给构造方法,不用像前面那样使用 * 拆包。
return cls(memv)
- 测试
Vector.__init__ 和 Vector.__repr__
方法:
if __name__ == '__main__':
print(repr(Vector([3.1, 4.2])))
print(repr(Vector([3, 4, 5])))
print(repr(Vector(range(10))))
# Vector([3.1, 4.2])
# Vector([3.0, 4.0, 5.0])
# Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
- 值得注意的是,我们使用了
reprlib.repr()
生成长度有限的表示形式。包含大量元素的集合类型一定要这么做,因为字符串表示形式是用于调试的。部分源码如下,迭代深度默认为6,6以及后面的部分使用省略号截断。
class Repr:
def __init__(self):
self.maxlevel = 6 # 迭代深度
self.maxtuple = 6
self.maxlist = 6
self.maxarray = 5
self.maxdict = 4
self.maxset = 6
self.maxfrozenset = 6
self.maxdeque = 6
self.maxstring = 30
self.maxlong = 40
self.maxother = 30
def repr(self, x):
return self.repr1(x, self.maxlevel)
...
def _repr_iterable(self, x, level, left, right, maxiter, trail=''):
n = len(x)
if level <= 0 and n:
s = '...'
else:
newlevel = level - 1
repr1 = self.repr1
pieces = [repr1(elem, newlevel) for elem in islice(x, maxiter)]
if n > maxiter: pieces.append('...')
s = ', '.join(pieces)
if n == 1 and trail: right = trail + right
return '%s%s%s' % (left, s, right)
- 在写
__repr__
方法时,我们使用的是切片。但是一般人首先想到的可能是使用list构造方法:
reprlib.repr(list(self._components))
- 然而,这么做有点浪费,因为要把 self._components 中的每个元素复制到一个列表中,然后使用列表的表示形式。这样做虽说浪费了性能,但是代码显得更简洁,实际使用时根据具体情况使用即可。
- 调用
repr()
函数的目的是调试,因此绝对不能抛出异常。如果__repr__
方法的实现有问题,那么必须处理,尽量输出有用的内容,让用户能够识别目标对象。 - 我们本可以让 Vector 继承 Vector2d,没有这么做的原因有二:其一,两个构造方法不兼容,因此不建议继承。这一点可以通过适当处理
__init__
方法的参数解决,不过第二个原因更重要:我想把 Vector 类当作单独的示例,以此实现序列协议。
接下来,我们先讨论协议这个术语,然后实现序列协议
。
2. 协议和鸭子类型
在 Python 中创建功能完善的序列类型无需使用继承,只需实现符合序列协议的方法。
不过,这里说的协议是什么呢?在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。
例如,Python 的序列协议只需要__len__
和__getitem__
两个方法。任何类(如 Spam),只要使用标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。这里直接用之前的示例如下:
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
"""扑克牌"""
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
self._cards = [Card(suit, rank) for suit in self.suits for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, item):
return self._cards[item]
- FrenchDeck 类能充分利用 Python 的很多功能,因为它实现了序列协议,不过代码中并没有声明这一点。任何有经验的 Python程序员只要看一眼就知道它是序列,即便它是 object 的子类也无妨。
我们说它是序列,因为它的行为像序列
,这才是重点。 - 回顾一下
鸭子类型
的定义:
多态的一种形式,在这种形式中,不管对象属于哪个类,也不管声明的具体接口是什么,只要对象实现了相应的方法,函数就可以在对象上执行操作。
- FrenchDeck 就是典型的鸭子类型。
协议是非正式的,没有强制力,因此如果你知道类的具体使用场景,通常只需要实现一个协议的部分。
例如,为了支持迭代,只需实现__getitem__
方法,没必要提供__len__
方法。 - 下面,我们将在 Vector 类中实现序列协议。我们先不支持完美的切片,稍后再完善。
3. Vector类第2版:可切片的序列
- 如 FrenchDeck 类所示,如果能委托给对象中的
序列属性
(如self._components 数组),支持序列协议特别简单。下述只有一行代码的__len__
和__getitem__
方法是个好的开始:
import math
import reprlib
from array import array
class Vector:
type_code = 'd'
def __init__(self, components):
self._components = array(self.type_code, components)
def __len__(self):
return len(self._components)
def __getitem__(self, item):
return self._components[item]
# 后续代码和之前一样
...
- 添加这两个方法后,就能执行如下操作了:
if __name__ == '__main__':
v1 = Vector([3, 4, 5])
print(len(v1)) # 3
print(v1[0], v1[-1]) # 3.0 5.0
v2 = Vector(range(7))
print(v2[1:4]) # array('d', [1.0, 2.0, 3.0])
- 可以看到,现在连切片都支持了,不过尚不完美。如果
Vector 实例的切片也是 Vector 实例,而不是数组
,那就更好了。前面那个FrenchDeck 类也有类似的问题:切片得到的是列表。对 Vector 来说,如果切片生成普通的数组,将会缺失大量功能。想想内置的序列类型,切片得到的都是各自类型的新实例,而不是其他类型。 - 为了把 Vector 实例的切片也变成 Vector 实例,我们不能简单地委托给数组切片。我们要分析传给
__getitem__
方法的参数,做适当的处理。下面来看 Python 如何把my_seq[1:3]
句法变成传给my_seq.__getitem__(...)
的参数。
3.1 切片原理
- 废话少说,先来看个例子:
class MySeq:
def __getitem__(self, index):
return index
if __name__ == '__main__':
s = MySeq()
print(s[1]) # 1
print(s[1:4]) # slice(1, 4, None)
print(s[1:4:2]) # slice(1, 4, 2)
print(s[1:4:2, 9]) # (slice(1, 4, 2), 9)
print(s[1:4:2, 7:9]) # (slice(1, 4, 2), slice(7, 9, None))
slice(1, 4, 2)
的意思是从 1 开始,到 4 结束,步幅(step)为 2,也就是切片间隔为2。值得注意的是:当 [] 中有逗号,那么__getitem__
收到的是元组
,元组中甚至可以有多个切片对象。- 现在,我们来仔细看看 slice 本身:
print(dir(slice))
'''
[
'__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop'
]
'''
- 通过审查内置对象 slice,发现它有 start、stop 和 step 数据属性,以及 indices 等方法。
indices
这个方法有很大的作用,但是鲜为人知。help(slice.indices)
给出的信息如下:
print(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.
翻译:给定长度为 len 的序列,计算 S 表示的扩展切片的起始(start)和结尾(stop)索引,
以及步幅(stride)。超出边界的索引会被截掉,这与常规切片的处理方式一样。
None
'''
- 换句话说,
indices 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。
这个方法会“整顿”元组,把 start、stop 和 stride 都变成非负数,而且都落在指定长度序列的边界内。下面举几个例子。假设有个长度为 5 的序列,例如 ‘ABCDE’:
print(slice(None, 5, 2).indices(5)) # (0, 5, 2)
print(slice(-3, None, None).indices(5)) # (2, 5, 1)
- 这就意味着:
'ABCDE'[:10:2] 等同于 'ABCDE'[0:5:2]
'ABCDE'[-3:] 等同于 'ABCDE'[2:5:1]
- 在 Vector 类中无需使用 slice.indices() 方法,因为收到切片参数时,我们会委托 _components 数组处理。但是,如果你没有底层序列类型作为依靠,那么使用这个方法能节省大量时间。现在我们知道如何处理切片了,下面来看
Vector.__getitem__
方法改进后的实现。
3.2 能处理切片的__getitem__方法
- 改进后的代码如下所示:
def __getitem__(self, item):
cls = type(self) # 获取实例所属的类
if isinstance(item, slice):
return cls(self._components[item])
elif isinstance(item, numbers.Integral):
return self._components[item]
else:
msg = '{cls.__name__} indices must be int integers'
raise TypeError(msg.format(cls=cls))
- 如果 item 的值是 slice 对象,就调用构造方法构建新的切片实例。大量使用 isinstance 可能表明面向对象设计得不好,不过在
__getitem__
方法中使用它处理切片是合理的。来看下测试结果:
if __name__ == '__main__':
v1 = Vector(range(7))
print(v1[-1]) # 6.0
print(repr(v1)) # Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
print(repr(v1[1:4])) # Vector([1.0, 2.0, 3.0])
print(repr(v1[-1:])) # Vector([6.0])
try:
print(v1[1, 2])
except TypeError as e:
print(e) # Vector indices must be integers
- 测试说明切片索引创建一个新 Vector 实例,就算切片的长度为1也是一样。Vector 不支持多维索引,因此索引元组或多个切片会抛出错误。
4. Vector类第3版:动态存取属性
- Vector目前还无法通过名称访问向量的分量(如 v.x 和 v.y),我们处理的向量可能有大量分量。若能通过单个字母访问前几个分量的话会比较方便。比如,用 x、y 和 z代替 v[0]、v[1] 和 v[2]。
- 如果我们想使用x,y,z,t 读取向量的前四个分量,我们可以在 Vector 中编写四个特性,使用
@property
装饰器把 x 和 y 标记为只读特性,但这样太麻烦。特殊方法__getattr__
提供了更好的方式。 属性查找失败后,解释器会调用 __getattr__ 方法。
简单来说,对my_obj.x
表达式,Python 会检查 my_obj 实例有没有名为 x 的属性;如果没有,到类(my_obj.__class__)
中查找;如果还没有,顺着继承树继续查找。 如果依旧找不到,调用 my_obj 所属类中定义的__getattr__
方法,传入 self 和属性名称的字符串形式(如’x’)。这样的话我们为 Vector 类定义__getattr__
方法。这个方法的作用很简单,它检查所查找的属性是不是 xyzt 中的某个字母,如果是,那么返回对应的分量。如下所示:
shortcut_names = 'xyzt'
def __getattr__(self, item):
"""检查所查找的属性是不是 xyzt 中的某个字母,如果是,那么返回对应的分量"""
cls = type(self)
if len(item) == 1:
index = cls.shortcut_names.find(item)
if 0 <= index <= len(self._components):
return self._components[index]
msg = '{.__name!r} object has no attribute {!r}'
raise AttributeError(msg.format(cls, item))
__getattr__
方法的实现不难,但是这样实现还不够。看看下面的测试就知道了:
if __name__ == '__main__':
v = Vector(range(5))
print(repr(v)) # Vector([0.0, 1.0, 2.0, 3.0, 4.0])
print(v.x) # 0.0
v.x = 10
print(v.x) # 10
print(repr(v)) # Vector([0.0, 1.0, 2.0, 3.0, 4.0])
- 测试结果可知为 v.x 赋新值,这个操作应该抛出异常,但是没有抛出异常,而且再次读取时得到的 x 为新值 10。可是向量的分量没有改变。这是为什么呢?
- 这其实是
__getattr__
的运作方式导致的:仅当对象没有指定名称的属性时,Python 才会调用那个方法,这是一种后备机制。
可是,像 v.x = 10 这样赋值之后,v 对象有 x 属性了。因此使用 v.x 获取 x 属性的值时不会调用__getattr__
方法了,解释器直接返回绑定到 v.x 上的值,即 10。另一方面,__getattr__
方法的实现没有考虑到self._components
之外的实例属性,而是从这个属性中获取shortcut_names
中所列的“虚拟属性”
。 - 为了避免这种前后矛盾的现象,我们要改写 Vector 类中设置属性的逻辑。为此,我们要实现
__setattr__
方法,如下所示:
def __setattr__(self, key, value):
cls = type(self)
if len(key) == 1:
if key in cls.shortcut_names:
error = 'readonly attribute {attr_name!r}'
elif key.islower():
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=key)
raise AttributeError(msg)
super().__setattr__(key, value) # 默认情况:在超类上调用 __setattr__ 方法,提供标准行为。
super()
函数用于动态访问超类的方法,对 Python 这样支持多重继承的动态语言来说,必须能这么做。程序员经常使用这个函数把子类方法的某些任务委托给超类中适当的方法
- 注意,我们没有禁止为全部属性赋值,只是禁止为单个小写字母属性赋值,以防与只读属性 x、y、z 和 t 混淆。测试如下:
if __name__ == '__main__':
v = Vector(range(5))
print(repr(v)) # Vector([0.0, 1.0, 2.0, 3.0, 4.0])
print(v.x) # 0.0
try:
v.x = 10
except AttributeError as e:
print(e) # readonly attribute 'x'
try:
v.c = 1
except AttributeError as e:
print(e) # can't set attributes 'a' to 'z' in 'Vector'
- 我们知道,在类中声明
__slots__
属性可以防止设置新实例属性;因此,你可能想使用这个功能,而不像这里所做的,实现__setattr__
方法。不建议只为了避免创建实例属性而使用__slots__
属性。__slots__
属性只应该用于节省内存,而且仅当内存严重不足时才应该这么做。(关于__slots__
的讲解可以查看我上一篇博客) - 虽然这个示例不支持为 Vector 分量赋值,但是有一个问题要特别注意:
多数时候,如果实现了 __getattr__ 方法,那么也要定义__setattr__ 方法,以防对象的行为不一致。
- 如果想允许修改分量,可以使用
__setitem__
方法,支持 v[0] =1.1 这样的赋值,以及(或者)实现__setattr__
方法,支持 v.x= 1.1 这样的赋值。
5. Vector类第4版:散列和快速等值测试
- 为了将Vector 实例变成可散列的对象,必须实现
__hash__
方法和__eq__
方法。现已实现__eq__
方法,只需要实现__hash__
方法。 - 为了计算 Vector 对象的散列值,我们要使用
^(异或)
运算符依次计算各个分量的散列值,例如hash(self.x) ^ hash(self.y)。
但是如果分量过多,又该怎样处理呢? - 为了实现多个分量的聚合,我们采用
归约函数:functools.reduce()
。下面来详细了解下这个规约函数。 functools.reduce() 的关键思想是,把一系列值归约成单个值
。reduce() 函数的第一个参数是接受两个参数的函数,第二个参数是一个可迭代的对象。假如有个接受两个参数的 fn 函数和一个 lst 列表。调用reduce(fn, lst)
时,fn 会应用到第一对元素上,即fn(lst[0],lst[1])
,生成第一个结果 r1。然后,fn 会应用到 r1 和下一个元素上,即fn(r1, lst[2])
,生成第二个结果 r2。接着,调用 fn(r2,lst[3]),生成 r3……直到最后一个元素,返回最后得到的结果 rN。- 你已经知道 reduce 的原理了,接下来看个例子加深理解,使用 reduce 函数计算 5!(5 的阶乘),这里我们使用不同的实现方式:
'''
计算 5!
'''
import functools
import operator
if __name__ == '__main__':
print(functools.reduce(lambda a, b: a * b, range(1, 6))) # 120
print(functools.reduce(operator.mul, range(1, 6))) # 120
- 了解了归约函数的用法,让那们回到主题,使用分量计算聚合散列值(hash值),如下示例,采用了不同的实现方式计算整数 0~5 的累计异或值。
import functools
import operator
'''
计算整数 0~5 的累计异或值
'''
if __name__ == '__main__':
# 实现方式1: 使用 for 循环和累加器变量计算聚合异或
n = 0
for i in range(1, 6):
n ^= i
print(n)
# 实现方式2: 使用 functools.reduce 函数,传入匿名函数
print(functools.reduce(lambda a, b: a ^ b, range(6))) # 1
# 实现方式3: 使用 functools.reduce 函数,把 lambda 表达式换成operator.xor
print(functools.reduce(operator.xor, range(6))) # 1
- 显然第三种实现方式最简洁,operator 模块以函数的形式提供了 Python 的全部中缀运算符,从而减少使用 lambda 表达式。
- 有了上面的基础,Vector 实现
__hash__
就容易多了,如下示例:
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)
- 注意:使用 reduce 函数时最好提供第三个参数,
reduce(function, iterable, initializer)
,这样能避免这个异常:TypeError: reduce() of empty sequence with no initial value
。如果序列为空,initializer是返回的结果;否则,在归约中使用它作为第一个参数,因此应该使用恒等值。
比如,对 +、| 和 ^ 来说, initializer 应该是0;而对 * 和 & 来说,应该是 1。 - 由于reduce 第二个参数需要的是可迭代的对象,所以 hashes 也可以使用 map 生成,如下示例:
def __hash__(self):
# hashes = (hash(x) for x in self._components)
hashes = map(hash, self._components)
return functools.reduce(operator.xor, hashes)
- Vector 散列测试示例如下:
if __name__ == '__main__':
print(hash(Vector([1, 3]))) # 2
print(hash(Vector([3.1, 4.2]))) # 384307168202284039
print(hash(Vector(range(6)))) # 1
- 既然讲到了归约函数,那就把前面实现的
__eq__
方法修改一下,减少处理时间和内存用量——至少对大型向量来说是这样。__eq__
实现方式要完整复制两个操作数,构建两个元组,而这么做只是为了使用tuple 类型的__eq__
方法。这样做对有几千个甚至更多分量的 Vector 实例来说,效率十分低下。为了提高比较的效率,Vector.__eq__
方法在 for循环中使用zip 函数
,如下示例:
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
- 这里
__eq__
作了两次判断,先判断是比较的两个对象长度是否相等,然后使用zip函数聚合各个分量,逐个判断分量是否相等。zip 函数生成一个由元组构成的生成器,元组中的元素来自参数传入的各个可迭代对象。
前面比较长度的测试是有必要的,因为一旦有一个输入耗尽,zip 函数会立即停止生成值,而且不发出警告。 - 这样实现的
__eq__
效率很好,不过看起来是不是显得过于繁琐,能不能只用一行代码进行处理呢?使用all()
内置函数可以实现这一点:如果对所有分量的比较结果都是 True,那么结果就是 True。只要有一次比较的结果是 False,all 函数就返回 False。
简化后的__eq__
如下所示:
def __eq__(self, other):
return len(self) == len(other) and all(a == b for a, b in zip(self, other))
- 使用 for 循环迭代元素不用处理索引变量,还能避免很多缺陷,但是需要一些特殊的实用函数协助。其中一个是内置的 zip 函数。zip 函数的名字取自拉链系结物(zipper fastener),因为这个物品用于把两个拉链边的链牙咬合在一起,这形象地说明了
zip(left, right)
的作用。zip 函数与文件压缩没有关系。下面来看下 zip 函数的使用用例:
if __name__ == '__main__':
print(zip(range(3), 'abc'))
# <zip object at 0x000001E9EC606F40>
print(list(zip(range(3), 'abc')))
# [(0, 'a'), (1, 'b'), (2, 'c')]
from itertools import zip_longest
print(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)]
- zip 函数返回一个生成器,按需生成元组。为了体现输出,这里构建了一个列表;通常,我们会迭代生成器。zip最大缺陷就是:
当一个可迭代对象耗尽后,它不发出警告就停止。
为了解决这个缺陷,在示例中引入了zip_longest
,这个函数使用可选的fillvalue(默认值为 None)填充缺失的值,因此可以继续产出,直到最长的可迭代对象耗尽。
6. Vector类第5版:格式化
- Vector 类的
__format__
不使用极坐标,而使用球面坐标
(也叫超球面坐标),因为 Vector 类支持 n 个维度,而超过四维后,球体变成了“超球体”。因此,我们会把自定义的格式后缀由 ‘p’ 变成 ‘h’。
扩展格式规范微语言时,最好避免重用内置类型支持的格式代码。
这里对微语言的扩展还会用到浮点数的格式代码 ‘eEfFgGn%’,而且保持原意,因此绝对要避免重用代码。整数使用的格式代码有 ‘bcdoxXn’,字符串使用的是’s’。在 Vector2d 类中,我选择使用 ‘p’ 表示极坐标。使用’h’ 表示超球面坐标是个不错的选择。
- 例如,对四维空间(len(v) == 4)中的 Vector 对象来说,‘h’ 代码得到的结果是这样:
<r, Φ1, Φ2, Φ3>
。其中,r 是模 abs(v),余下三个数是角坐标 Φ1、Φ2 和 Φ3。 - 在改动
__format__
方法之前,我们要定义两个辅助方法:一个是 angle(n),用于计算某个角坐标(如 Φ1);另一个是angles(),返回由所有角坐标构成的可迭代对象。如下所示
def angle(self, n):
"""计算n维球体中的某个角坐标"""
r = math.sqrt(sum(x * x for x in self[n:])) # 求模 r
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)))
- 有了这两个方法,n 维球坐标 (Vector) 的
__format__
就容易多了,如下所示:
def __format__(self, format_spec=''):
if format_spec.endswith('h'): # h 这里为n维球坐标的标识
format_spec = format_spec[:-1]
# 使用 itertools.chain 函数生成生成器表达式,无缝迭代向量的模和各个角坐标。
coords = itertools.chain([abs(self)], self.angles())
out_fmt = '<{}>' # 使用尖括号显示球面坐标
else:
coords = self
out_fmt = '({})' # 使用圆括号显示笛卡儿坐标
components = (format(c, format_spec) for c in coords)
return out_fmt.format(', '.join(components))
- 接下来测试不同情况格式化 Vector 的表现:
if __name__ == '__main__':
v = Vector([1, 2, 3, 4])
print(format(v))
print(format(v, 'h'))
# (1.0, 2.0, 3.0, 4.0)
# <5.477225575051661, 1.387192316515978, 1.1902899496825317, 0.9272952180016122>
v2 = Vector(range(5))
print(format(v2))
print(format(v2, '.2f'))
print(format(v2, '.3eh'))
print(format(v2, '.5fh'))
# (0.0, 1.0, 2.0, 3.0, 4.0)
# (0.00, 1.00, 2.00, 3.00, 4.00)
# <5.477e+00, 1.571e+00, 1.387e+00, 1.190e+00, 9.273e-01>
# <5.47723, 1.57080, 1.38719, 1.19029, 0.92730>
- 最后的 Vector 完整代码如下
import math
import reprlib
import numbers
import functools
import operator
import itertools
from array import array
class Vector:
type_code = 'd'
shortcut_names = 'xyzt'
def __init__(self, components):
self._components = array(self.type_code, components)
def __len__(self):
return len(self._components)
def __getitem__(self, item):
cls = type(self)
if isinstance(item, slice):
return cls(self._components[item])
elif isinstance(item, numbers.Integral):
return self._components[item]
else:
msg = "{cls.__name__} indices must be integers"
raise TypeError(msg.format(cls=cls))
def __setattr__(self, key, value):
cls = type(self)
if len(key) == 1:
if key in cls.shortcut_names:
error = 'readonly attribute {attr_name!r}'
elif key.islower():
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=key)
raise AttributeError(msg)
super().__setattr__(key, value) # 默认情况:在超类上调用 __setattr__ 方法,提供标准行为。
def __getattr__(self, item):
"""检查所查找的属性是不是 xyzt 中的某个字母,如果是,那么返回对应的分量"""
cls = type(self)
if len(item) == 1:
index = cls.shortcut_names.find(item)
if 0 <= index <= len(self._components):
return self._components[index]
msg = '{.__name!r} object has no attribute {!r}'
raise AttributeError(msg.format(cls, item))
def __iter__(self):
# 为了迭代,我们使用 self._components 构建一个迭代器
return iter(self._components)
def __repr__(self):
# 使用 reprlib.repr() 函数获取 self._components 的有限长度表示形式(如 array('d', [0.0, 1.0, 2.0, 3.0, 4.0,...]))。
components = reprlib.repr(self._components)
# 把字符串插入 Vector 的构造方法调用之前,去掉前面的array('d' 和后面的 )。
components = components[components.find('['):-1]
return 'Vector({})'.format(components)
def __str__(self):
return str(tuple(self))
def __bytes__(self):
# 直接使用 self._components 构建 bytes 对象。
return bytes([ord(self.type_code)]) + bytes(self._components)
def __eq__(self, other):
return len(self) == len(other) and all(a == b for a, b in zip(self, other))
def __hash__(self):
# hashes = (hash(x) for x in self._components)
hashes = map(hash, self._components)
return functools.reduce(operator.xor, hashes)
def __abs__(self):
# 不能使用 hypot(平方根) 方法了,因此我们先计算各分量的平方之和,然后再使用 sqrt 方法开平方。
return math.sqrt(sum(x * x for x in self))
def __bool__(self):
return bool(abs(self))
def angle(self, n):
"""计算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, format_spec=''):
if format_spec.endswith('h'): # h 这里为n维球坐标的标识
format_spec = format_spec[:-1]
# 使用 itertools.chain 函数生成生成器表达式,无缝迭代向量的模和各个角坐标。
coords = itertools.chain([abs(self)], self.angles())
out_fmt = '<{}>' # 使用尖括号显示球面坐标
else:
coords = self
out_fmt = '({})' # 使用圆括号显示笛卡儿坐标
components = (format(c, format_spec) for c in coords)
return out_fmt.format(', '.join(components))
@classmethod
def frombytes(cls, b):
type_code = chr(b[0])
memv = memoryview(b[1:]).cast(type_code)
# 因为直接传入序列(components),把 memoryview 传给构造方法,不用像前面那样使用 * 拆包。
return cls(memv)
小结
- 本篇博客可以说是上一篇博客的延伸,Vector 的行为之所以像序列,是因为它实现了
__getitem__
和__len__
方法;借此,我们讨论了协议,这是鸭子类型语言使用的非正式接口。 - 我们说明了
my_seq[a:b:c]
句法背后的工作原理:创建slice(a, b, c) 对象,交给__getitem__
方法处理。了解这一点之后,我们让 Vector 正确处理切片,像符合 Python 风格的序列那样返回新的 Vector 实例。 - 我们为 Vector 实例的头几个分量提供了只读访问功能,使用my_vec.x 这样的表示法。这一点通过
__getattr__
方法实现。大多数时候,如果定义了__getattr__
方法,那么也要定义__setattr__
方法,这样才能避免行为不一致。 - 实现
__hash__
方法特别适合使用 functools.reduce 函数,因为我们要把异或运算符 ^ 依次应用到各个分量的散列值上,生成整个向量的聚合散列值。在__hash__
方法中使用 reduce 函数之后,我们又使用内置的归约函数 all 实现了效率更高的__eq__
方法。 - Vector 类的最后一项改进是实现
__format__
方法,除了支持笛卡儿坐标,我们还支持了球面坐标。 - 总而言之,我们分析 Python 标准对象的行为,然后进行模仿,让 Vector 的行为符合 Python 风格。