本章的notebook文件在这里

序列的修改,散列和切片

不要检查它是不是鸭子,它的叫声像不像鸭子,它的走路姿势像不像鸭子等等。具体检查什么取决于你想使用语言的哪些行为。
——Alex Matrelli

这一章来看看如何构造一个序列类型,我们以Vector类为例。

协议与鸭子类型

在Python中创建功能完善的序列类型无需使用继承,只需实现符合序列协议的方法,这里协议是指面向对象编程中,只在文档中定义而不在代码中定义的非正式接口。

例如,python的序列协议需要__len____getitem__两个方法, 任何实现了这两个方法的类就能在期待序列的地方使用。

在编程中,鸭子类型指的是,符合鸭子特性的类型,即所谓的“长得像不像鸭子,走路像不像鸭子,叫声像不像鸭子”,但是在python中,只要符合了协议的类都可以算作鸭子类型。也就是我们开头所说的那句话。
我们想实现一个鸭子(序列),只需要检查有没有鸭子的这些协议(序列的__len____getitem__)。

可切片的序列

下面我们来实现一个可以切片的Vector序列,其中我们需要注意的是:

  • 需要实现__len____getitem__
  • __getitem__返回的最好也是Vector实例

回顾以下切片类slice的使用:

  • slice需要给定三个参数,start,stop和stride,分别表示开始位,结束位和步幅,步幅默认为1,开始位默认为0
  • slice类的indices函数接收一个长度参数len并由此对slice的三元组进行整顿,例如大于长度的stop置换为stop,处理负数参数等等

举个例子:

1
slice(-5, 20, 2).indices(15)
(10, 15, 2)

上例中,indices函数就将负数的start和超出长度(15)的stop(20)做了重整。当你不依靠底层序列类型来实现自己的序列时,充分利用该函数就能节省大量时间。

然后我们来实现这个能处理切片的序列,为了简洁我省略了部分与切片无关的代码:

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
import numbers
import reprlib
from array import array

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 __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))
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]

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

当然这样会有一个问题,你再对`x`进行赋值是可以正常实现的,但是以后再访问`x`得到的就都是这个后来赋的值了,和预期就不一样了。

这里我们需要防止对这些实例属性赋值,所以要实现如下`__setattr__`:

```py
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():
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)

散列和快速等值测试

这里我们来实现__hash__方法,算法上我们还是用异或来算,但是和之前的二维向量不同,这里我们的异或需要作用在所有向量元素上。
这里可以有几种方式来实现,下面以计算1~6的异或为例:

1
2
3
4
n = 0 
for i in range(1, 6):
n ^= i
print(n)

1

1
2
import functools
functools.reduce(lambda a, b: a^b, range(6))
1
1
2
import operator  
functools.reduce(operator.xor, range(6))
1

那么这里我们也可以类似的实现__hash__

1
2
3
def __hash__(self):
hashed = (hash(x) for x in self._components)
return functools.reduce(operator.xor, hashes, 0)

这里既然用到了规约函数,那么同样的我们也可以用zip函数来将__eq__拓展到多维:

1
2
3
4
5
6
7
def __eq__(self, other):
if len(self) != len(other):
return False
for a, b in zip(self, other):
if a != b:
return False
return Truen

最后,我们完整的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
87
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 (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))

@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(memv)