Python拾遗 鸭子对象 序列的切片与散列 --流畅的python
本章的notebook文件在这里
序列的修改,散列和切片
不要检查它是不是鸭子,它的叫声像不像鸭子,它的走路姿势像不像鸭子等等。具体检查什么取决于你想使用语言的哪些行为。
——Alex Matrelli
这一章来看看如何构造一个序列类型,我们以Vector类为例。
协议与鸭子类型
在Python中创建功能完善的序列类型无需使用继承,只需实现符合序列协议的方法,这里协议是指面向对象编程中,只在文档中定义而不在代码中定义的非正式接口。
例如,python的序列协议需要__len__
和__getitem__
两个方法, 任何实现了这两个方法的类就能在期待序列的地方使用。
在编程中,鸭子类型指的是,符合鸭子特性的类型,即所谓的“长得像不像鸭子,走路像不像鸭子,叫声像不像鸭子”,但是在python中,只要符合了协议的类都可以算作鸭子类型。也就是我们开头所说的那句话。
我们想实现一个鸭子(序列),只需要检查有没有鸭子的这些协议(序列的__len__
和__getitem__
)。
可切片的序列
下面我们来实现一个可以切片的Vector序列,其中我们需要注意的是:
- 需要实现
__len__
和__getitem__
__getitem__
返回的最好也是Vector
实例
回顾以下切片类slice
的使用:
slice
需要给定三个参数,start,stop和stride,分别表示开始位,结束位和步幅,步幅默认为1,开始位默认为0slice
类的indices
函数接收一个长度参数len
并由此对slice
的三元组进行整顿,例如大于长度的stop置换为stop,处理负数参数等等
举个例子:
1 | slice(-5, 20, 2).indices(15) |
(10, 15, 2)
上例中,indices
函数就将负数的start和超出长度(15)的stop(20)做了重整。当你不依靠底层序列类型来实现自己的序列时,充分利用该函数就能节省大量时间。
然后我们来实现这个能处理切片的序列,为了简洁我省略了部分与切片无关的代码:
1 | import numbers |
1 | v = Vector(range(7)) |
1 | v[-1] |
6.0
1 | v[1:4] |
Vector([1.0, 2.0, 3.0])
1 | v[-1:] |
Vector([6.0])
1
2
# 尝试这样切片就会抛出错误
v[1,2]
1 | # 尝试这样切片就会抛出错误 |
TypeError Traceback (most recent call last)
<ipython-input-26-089a4a83def1> in <module>()
----> 1 v[1,2]
<ipython-input-21-c041f698a9b2> in __getitem__(self, index)
29 else:
30 msg = '{.__name__} indices must be integers'
---> 31 raise TypeError(msg.format(cls))
TypeError: Vector indices must be integers
动态存取属性
对于上述的Vector类,我们想通过x,y,z,t
属性分别来访问向量的前四个分量(如果有的话)。这里可以用之前的@property
装饰器把它们标记为只读属性,但是四个属性一个一个写就很麻烦。
这里我们可以用特殊方法__getattr__
来处理这个问题。
在原来的实现上加入以下代码就可:
1 | shortcut_names = 'xyzt' |
散列和快速等值测试
这里我们来实现__hash__
方法,算法上我们还是用异或来算,但是和之前的二维向量不同,这里我们的异或需要作用在所有向量元素上。
这里可以有几种方式来实现,下面以计算1~6的异或为例:1
2
3
4n = 0
for i in range(1, 6):
n ^= i
print(n)
1
1 | import functools |
1
1 | import operator |
1
那么这里我们也可以类似的实现__hash__
1 | def __hash__(self): |
这里既然用到了规约函数,那么同样的我们也可以用zip
函数来将__eq__
拓展到多维:
1 | def __eq__(self, other): |
最后,我们完整的Vector
类将是这样的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87class 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 (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)
return functools.reduce(operator.xor, hashes, 0)
def __abs__(self):
return math.sqrt(sum(x * x for x in self))
def __bool__(self):
return bool(abs(self))
def __len__(self):
return len(self._components)
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 = '{.__name__} indices must be integers'
raise TypeError(msg.format(cls))
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))
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'): # hyperspherical coordinates
fmt_spec = fmt_spec[:-1]
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))
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(memv)