本章以第9章定义的二维向量Vector2d类为基础,定义表示多维向量的Vector类。这个类的行为与Python中标准的不可变扁平序列一样。Vector 实例中的元素是浮点数,本章结束后 Vector 类将支持下述功能:
- 基本的序列协议 -----
__len__
和__getitem__
- 正确表述拥有很多元素的实例
- 适当的切片支持,用于生成新的 Vector 实例
- 综合各个元素的值计算散列值
- 自定义的格式语言扩展
此外,我们还将通过 __getattr__
方法实现属性的动态存取,以此取代 Vector2d 使用的只读特性。
在大量代码之间,我们将穿插讨论一个概念:把协议当作正式接口。我们将说明协议和鸭子类型之间的关系,以及对自定义类型的实际影响。
Vector类第一版:与Vector2d类兼容
from array import array
import reprlib
import mathclass 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))@classmethoddef frombytes(cls, octets):typecode = chr(octets[0])memv = memoryview(octets[1:]).cast(typecode)return cls(memv) # 不用解包
协议和鸭子类型
在 Python 中创建功能完善的序列类型无需使用继承,只需实现符合序列协议的方法。不过,这里说的协议是什么呢?
协议是非正式的接口,只在文档中定义,在代码中不定义。例如,Python 的序列协议只需要 __len__
和 __getitem__
两个方法。任何类(如Spam), 只需使用标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。
import collectionsCard = 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(rank, suit) for suit in self.suitsfor rank in self.ranks]def __len__(self):return len(self._cards)def __getitem__(self, position):return self._cards[position]
FrenchDeck
类能充分利用 Python 的很多功能,因为它实现了序列协议,不过代码中并没有声明这一点。我们说它是序列,因为它的行为像序列,这才是重点。
人们称其为鸭子类型(duck typing)。协议是非正式的,没有强制力。
下面,我们将在 Vector 类中实现序列协议。
Vector类第2版:可切片的序列
如 FrenchDeck
类所示,如果能委托给对象中的的序列属性(如 self._components
数组), 支持序列协议特别简单。
class Vector:typecode = 'd'def __len__(self):return len(self._components)def __getitem__(self, index):return self._components[index]
v = Vector([i for i in range(1, 30)])
print(len(v))
print(v[0])
print(v[1:3])""" output: 29 1.0 array('d', [2.0, 3.0]) """
可以看到,现在连切片都支持了,但尚不完美。Vector 实例的切片类型是数组而非 Vecotor,这样对 Vector 来说,它的切片将会确实大量功能。
为了把 Vector 实例的切片也变成 Vector 实例,我们不能简单地委托给数组切片。我们要分析传给 __getitem__
方法的参数,做适当的处理。
切片原理
一例胜千言,我们看看下面的示例:
>>> class MySeq:
... def __getitem__(self, index):
... return index
...
>>> s = MySeq()
>>> s[1]
1
>>> s[1:4]
slice(1, 4, None)
>>> s[1:4:2]
slice(1, 4, 2)
>>> s[1:4:2, 9]
(slice(1, 4, 2), 9)
现在,我们来看看 slice
本身:
>>> slice
<type 'slice'>
>>> dir(slice)
['__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop']
slice
是内置的类型, 发现它有 start
, stop
, 和 step
数据属性,以及 indices
方法。
indices
是一个很有用的方法,我们使用help(slice.indices)
来看看它的作用。
indices(...)S.indices(len) -> (start, stop, stride)Assuming a sequence of length len, calculate the start and stopindices, and the stride length of the extended slice described byS. Out of bounds indices are clipped in a manner consistent with thehandling of normal slices.
换句话说, indices
方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法会整顿元祖,把 start
, stop
和 stride
都变成非负数,而且都落在指定长度序列的边界内。
在 Vector 类中无需使用 slice.indices()
方法,因此受到切片参数时,我们会委托 _components
数组处理。但是,你如果没有底层序列类型作为依靠,那么使用这个方法能节省大量时间。
现在我们知道如何处理切片了,下面来看 Vector.__getitem__
方法改进后的实现。
能处理切片的 __getitem__
方法。
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 = '{cls.__name__} indices must be integers'raise TypeError(msg.format(cls=cls))
Vector类第3版:动态存取属性
Vector2d 变成 Vector 之后,就没办法通过名称访问向量的分量了(如 v.x 和 v.y).现在我们处理的向量可能有大量分量,不过,若能通过单个字母访问前几个分量的话会比较方便。比如,用x,y和z代替 v[0], v[1] 和 v[2]。
在 Vector2d
中, 我们使用 @property
装饰器把 x 和 y 标记为只读特性。我们可以在 Vector
中编写四个特性,但这样太麻烦。特殊方法 __getattr__
提供了更好的方式。
属性查找失败后,解释器会调用 __getattr__
方法。简单来说,对 my_obj.x
表达式, Python 会检查 my_obj
实例有没有名为 x
的属性:如果没有,到类(my_obj.__class__
)中查找;如果还没有,顺着继承树继续查找。如果依旧找不到,调用 my_obj
所属类中定义的 __getattr__
方法,传入 self
和属性名称的字符串形式(如'x'
)
下例我们将实现 Vector 类定义的 __getattr__
方法,这个方法的作用很简单,它检查所查找的属性是不是 xyzt
中的某个字母,如果是,那么返回对应的分量。
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))
这样带来了一个问题,如果Vector有x属性后,那么再来查找的话,这个逻辑就失效了。为了避免歧义,在 Vector 类中,如果为名称是单个小写字母的属性赋值,我们也想抛出那个异常。为此,我们要实现 __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():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)
Vector 类第4版:散列和快速等值测试
这个貌似没有什么好讲的,主要是内置的函数的使用。
def __eq__(self, other):if len(self) != len(other):return Falsefor a, b in zip(self, other):if a != b:return Falsereturn Truedef __hash__(self):hashes = map(hash, self._components)return functools.reduce(operator.xor, hashes)
格式化的内容就忽略啦,主要就是 __format__
方法的实现。
接下来我们将介绍“接口:从协议到抽象基类”