目录
前言
一、Vector类:用户定义的序列类型
?二、Vector类第一版:与Vector2d兼容
三、协议和鸭子类型
四、Vector类第2版:可切片的序列
把类序列协议的实现委托给其属性
切片原理
__getitem__和slice
Vector类中能处理切片的__getitem__方法
五、Vector类第三版:动态存取属性
六、Vector第四版:散列和快速等值测试
__hash__
__eq__
zip函数
七、Vector类第五版:格式化
前言
本章定义表示多维向量的Vector类,其中的元素是浮点数,将支持以下功能:
- 基本的序列协议------__len__和__getitem__
- 正确表述拥有很多元素的实例
- 适当的切片支持,用于生成新的Vector实例
- 综合各个元素的值计算散列值
- 自定义的格式语言扩展
此外,还将通过__getattr__方法实现属性的动态存取,虽然序列类型通常不会这么做。
一、Vector类:用户定义的序列类型
二、Vector类第一版:与Vector2d兼容
这里故意不让Vector的构造方法和Vector2d的构造方法兼容,一个用可迭代对象,一个是直接传入各个分量。
????????为了编写Vector(3,4)和Vector(3,4,5)这样代码,可以让构造函数__init__接受任意个参数(通过*args),但 是所有内置序列类型的做法都是让构造函数接收可迭代对象为参数。如下实例化方式所示:

如果Vector实例的分量超过6个,repr()生成的字符串就会用 ... 省略一部分,因为对象的字符串表示形式都是用于调试的,不能在控制台一次输出几千行内容,使用reprlib模块可以生成长度有限的表示形式。
? ? ? ? Vector类第一版代码如下:
from array import array
import reprlib
import math
class Vector:
typecode = 'd'
def __init__(self, components):
self._components = array(self.typecode, components) # 把Vector实例分量保存在一个float数组中,且self._components约定是受保护的属性
def __iter__(self):
return iter(self._components) # 使得Vector实例可迭代,委托给数组实现
def __repr__(self):
components = reprlib.repr(self._components) # 使用reprlib.repr函数获取self._components的有限长度表示形式
components = components[components.find('['):-1] # 上一行得到的components形式是array('d', [ ... ]), 这里里层components.find('[')找到左边的'['的位置,而-1是最后一个位置,即‘(’所在的位置索引,然后对外层的components切片,得到 [ ... ]的表示形式,下一行用于返回Vector的字符串表示形式
return 'Vector({})'.format(components)
def __str__(self):
return str(tuple(self)) # 先把数组转换成元组形式,再求字符串表示
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(self._components)) # 构建bytes对象
def __eq__(self, other):
return tuple(self) == tuple(other)
def __abs__(self):
return math.sqrt(sum(x * x for x in self)) # hypot只能计算二维欧氏距离,现在是多维
def __bool__(self):
return bool(abs(self))
@classmethod
def frombytes(cls, cotets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode) # 强转成float格式的内存视图
return cls(memv) # 现在的构造函数直接支持可迭代对象,所以不用像前章时需要*memv拆包

三、协议和鸭子类型
在python中创建功能完善的序列类型无需使用继承,只需实现符合序列协议的方法,协议是指?
? ? ? ? 在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。python中的序列协议只需要__len__和__getitem__两个方法,任何类,只要实现了这两个方法,就可以用在任何期待序列的地方,即此类可以归类为序列类型。
????????
像这个指派类,因为实现了__len__和__getitem__方法,我们就说它是序列,因为它的行为像序列,因此它就是序列。
? ? ? ? 这就是所谓的鸭子类型,行为像鸭子,就是鸭子。
? ? ? ? 协议是非正式的,没有强制力,在具体的使用场景,通常只需要实现一个协议的部分。例如为了支持迭代,只需要实现__getitem__方法,不必实现__len__方法。
四、Vector类第2版:可切片的序列
把类序列协议的实现委托给其属性
如果对象中有属性是序列,可以把实现类的__len__和__getitem__方法委托给这个序列属性。如下,我们将Vector类的序列协议实现委托给了属性self._components,而该属性本身是序列类型。

?但上边例子有缺点,即Vector实例切片的结果时一个数组对象,而不是Vector对象。? ?
切片原理
__getitem__和slice
看如下示例,了解__getitem__和切片的行为:

- 自定义__getitem__,直接返回传给它的值,接下来我们会看到当我们用索引或者切片时传给item的究竟是什么。
- 当索引切片对象时,__getitem__收到的值就是一个索引值整数。
- 当给序列进行切片时,__getitem__收到的值是一个slice对象,即切片对象,这也没有步长。
- 当进行带步长的切片时,__getitem__收到的切片对象也是带步长的
- 如果切片时有逗号,那么__getitem__收到的是元组,且这里元组的第二个元素是整数
- 这里元组的两个元素都是切片对象
slice类的属性如下:

- slice是python内置类型。
- slice中有start、step、stop等数据属性,以及indices方法。
注意slice的indices方法,其可以返回一个元组(start, stop, stride),用法是:
slice(s, t, st).indices(len) -> (start, stop, stride)
作用是对切片对象进行整顿,即优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。该方法把start,stop和stride都变成非负数,并且都落在指定长度序列的边界内。如下例子所示:


Vector类中能处理切片的__getitem__方法
因为上边实现的__getitem__方法中,切片返回的是数组而不是Vector对象,因此需要改进。


- ?获取实例所属的类,以供后边构造Vector对象返回。
- ?判断__getitem__方法收到的是不是slice对象,如果是则是求切片。
- 调用Vector类构造函数,使用_components数组切片构造一个新的Vector实例。
- 如果__getitem__收到参数是整数
- 则返回_components数组中对应索引处的元素
- __getitem__收到参数非slice非整数,则抛出异常
五、Vector类第三版:动态存取属性
Vector2d中只有两个分量,因此可以用v.x和v.y来分别访问两个分量,但是多维向量类Vector不能这样干了,如果想要用四个变量x、y、z、t来访问向量对象中的前四个分量,可以用特殊方法__getattr__来实现。具体属性查找过程大致如下:
- 对于my_obj.x表达式,先检查my_obj实例中有没有名为x的属性
- 如果上一步没找到,则到my_obj.__class__中去查找有没有名为x的类属性,因为类属性可以作为实例属性的默认值
- 如果上一步还是没找到,则沿着继承树继续查找......
- 如果依旧找不到,才会调用__getattr__方法,__getattr__有两个参数,第一个是self,第二个是'x',即属性名的字符串形式
如下图是一个__getattr__简单实现:
shortcut_names = 'xyzt'
def __getattr__(self, name):
cls = type(self) # 获取当前类名Vector
if len(name) == 1: # 属性名只有一个字符,可能是'xyzt'中的某一个
pos = cls.shortcut_names.find(name) # 查找是否是'xyzt'中的某一个
if 0 <= pos < len(self._components): # 定位位置
return self._components[pos]
msg = '{.__name__!r} object has no attribute {!r}' # 失败则抛出属性错误
raise AttributeError(msg.format(cls, name))
如上边__getattr__的实现,相当于增加了几个虚拟属性 ‘xyzt’,但是如果只实现__getattr__而没有实现__setattr__的话,可能会导致跟我们设想不同的行为。如下:

- 用v.x获取向量第一个元素,由于实现了__getattr__,x相当于是一个虚拟属性,因此访问成功
- 给v.x赋新值
- 可以看到v.x的值改为10
- 发现向量中第一个元素还是0,没有变成10!!!
上边现象出现的原因是,只有当对象查找不到某个属性时,才会调用__getattr__方法。标号1处用v.x访问时,x还是虚拟属性,而这里标号2处给v.x赋值时,会创建一个实例属性x出来,不再是虚拟属性,且给该属性赋值为10,而并没有改变原_components数组的值。为了避免这种行为,我们应该实现__setattr__,即设置改变属性时候的行为。


?为了下一小节讲散列,这里我们需要设置为Vector是只读的。
- 特别处理名字是单个字符的属性
- 如果name是xyzt中的一个,设置错误消息,提醒这些属性只读
- 如果name是一个小写字符,设置对应的错误消息
- 否则,错误消息设置为空字符串
- 如果有错误消息,则抛出AttributeError
- 默认情况下调用超类的__setattr__方法,提供标准行为
虽然在类中声明__slots__属性可以防止设置新实例属性,但是不建议这么做,只有在为了节省内存时才应该使用__slots__。
六、Vector第四版:散列和快速等值测试
__hash__
把各个分量的散列值异或起来,构成整个向量对象的散列值。
这里要用到一个函数,规约函数functools.reduce,其作用是将可迭代对象中前两个元素送入一个二参数函数中计算,然后得到的结果与第三个元素一起再送入二参数函数计算......以此类推。用法为:?
ans = functools.reduce(func, iter, initializer)
其中func是用来计算的二参数函数,比如加法,乘法等,iter是可迭代对象,initializer是当可迭代对象为空时候返回的初始值。
? ? ? ? 我们已知operator模块以函数的形式提供了python的全部中缀运算符,因此可以导入operator模块方便代码编写。
? ? ? ? 下边是用三种方式计算0~5的累计异或值,第一种使用for循环,后两种使用reduce函数。

可以看到使用operator模块的话就不用写匿名函数了,直接调用xor。
? ? ? ? 因此Vector类中的__hash__方法如下:

?标号4处特地创建了一个生成器表达式。
? ? ? ? 上例中__hash__函数是一种映射规约计算,即映射过程计算各个分量散列值,规约过程则使用xor运算符聚合所有散列值。
- 映射(map):把hash函数映射到序列的每一个元素上,得到一个新的序列值。
- 规约(reduce):用xor运算符聚合所有的元素。
可以把生成器表达式替换成map方法,映射过程更明显:


__eq__
__hash__和__eq__方法需要同时实现。
? ? ? ? 上边实现的__eq__方法,需要完成复制两个参数,生成两个元组,然后利用tuple的__eq__方法来判断,如果Vector实例有几千个分量的话,这种做法效率就非常低了。可以使用zip函数来重新实现。如下:

- 如果两个对象长度不一样,则它们不相等
- zip函数可以生成一个元素是元组的生成器(zip对象),然后拆包给变量a,b
- 只要有一个分量不同,就直接返回False
- 否则返回True
还可以用all函数来替换标号2处的for循环,all中是一个生成器表达式,只有生成的元素都是True的时候才返回True,否则返回False:

zip函数
zip函数用于并行迭代两个或更多的可迭代对象,返回一个zip对象(生成器),可以拆包。
a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]
z = zip(a, b, c)
print(type(z))
print(z)
print(list(z))

zip有一个奇怪的特性:当一个可迭代对象耗尽后,它不发出警告就停止,解决这个问题可以用itertools.zip_longest函数,其可以使用可选的默认值来填充缺失的值,然后继续产出直到最长的可迭代对象耗尽。

七、Vector类第五版:格式化
略。
?
?
?
?
|