流畅的python读书笔记-第十章-序列的修改、散列和切片
序列的修改、散列和切片
接着造Vector2d类
要达到的要求
为了编写Vector(3, 4) 和 Vector(3, 4, 5) 这样的代码,我们可以让 init 法接受任意个参数(通过 *args)
如果 Vector 实例的分量超过 6 个,repr() 生成的字符串就会使用 ... 省略一部
分,使用 reprlib 模块可以生成长度有限的表示形式
from array import array import reprlib import math 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) print(Vector([3.1, 4.2])) print(Vector((3, 4, 5))) print(Vector(range(10)))
❸ 使用 reprlib.repr() 函数获取 self._components 的有限长度表示形式(如
array('d', [0.0, 1.0, 2.0, 3.0, 4.0, ...]))。
❹ 把字符串插入 Vector 的构造方法调用之前,去掉前面的 array('d' 和后面的 )。
协议和鸭子类型
在面向对象编程中,
- 协议是非正式的接口,只在文档中定义,在代码中不定义。
- 例如,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(rank, suit) for suit in self.suits for rank in self.ranks] def __len__(self): return len(self._cards) def __getitem__(self, position): return self._cards[position]
Vector类第2版:可切片的序列
from array import array import reprlib import math class Vector(object): 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)) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(memv) def __len__(self): return len(self._components) def __getitem__(self, index): return self._components[index] v1 = Vector([3, 4, 5]) print(len(v1)) print(v1[0], v1[-1]) v7 = Vector(range(7)) print(v7[1:4])
现在连切片都支持了,不过尚不完美。如果 Vector 实例的切片也是 Vector
实例,而不是数组,那就更好了。
把 Vector 实例的切片也变成 Vector 实例,我们不能简单地委托给数组切片。我们
要分析传给 getitem 方法的参数,做适当的处理。
切片原理
class MySeq: def __getitem__(self, index): return index s = MySeq() print(s[1]) print(s[1:4]) print(s[1:4:2]) print(s[1:4:2, 9]) print(s[1:4:2, 7:9])
❸ 1:4 表示法变成了 slice(1, 4, None)。
❹ slice(1, 4, 2) 的意思是从 1 开始,到 4 结束,步幅为 2。
❺ 神奇的事发生了:如果 [] 中有逗号,那么 getitem 收到的是元组。
❻ 元组中甚至可以有多个切片对象。
查看 slice 类的属性
print(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 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。
这个方法会“整顿”元组,把 start、stop 和
stride 都变成非负数,而且都落在指定长度序列的边界内。
一句话 把负数索引和超出长度的索引调整成 正常的索引
aa = 'ABCDE' print(slice(None, 10, 2).indices(5)) print(slice(-3, None, None).indices(5)) print('='*40) print(slice(None, 10, 2).indices(len(aa))) print(slice(-3, None, None).indices(len(aa))) print(aa[-3:])
能处理切片的__getitem__方法
from array import array import reprlib import math import numbers class Vector(object): 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) ##[1:4] 返回一个向量对象 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)) v7 = Vector(range(7)) print(v7[-1]) print(v7[1:4]) print(v7[-1:])
Vector类第3版:动态存取属性
我们可以在 Vector 中编写四个特性,但这样太麻烦。
特殊方法 getattr 提供了更好的方式。
属性查找失败后,解释器会调用 getattr 方法。
简单来说,对 my_obj.x 表达式,
Python 会检查 my_obj 实例有没有名为 x 的属性;
如果没有,到类(my_obj.__class__)中查找;
如果还没有,顺着继承树继续查找。
如果依旧找不到,调用 my_obj 所属类中定义的 getattr 方法,传入 self 和属性名称的字符串形式(如 'x')。
from array import array import reprlib import math import numbers class Vector(object): 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) 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 __setattr__(self, name, value): cls = type(self) if len(name) == 1: # 如果 name 是 xyzt 中的一个,设置特殊的错误消息。 if name in cls.shortcut_names: error = 'readonly attribute {attr_name!r}' # 如果 name 是小写字母,为所有小写字母设置一个错误消息。 elif name.islower(): error = "can't set attributes 'a' to 'z' in {cls_name!r}" #否则,把错误消息设为空字符串。 else: error = '' #如果有错误消息,抛出AttributeError。 if error: msg = error.format(cls_name=cls.__name__, attr_name=name) raise AttributeError(msg) # 默认情况:在超类上调用 __setattr__ 方法,提供标准行为。 super().__setattr__(name, value) v = Vector(range(5)) print(v) # 这个设置法 没用 v.p = 10 print(v.x) print(v)
super() 函数用于动态访问超类的方法,对 Python 这样支持多重继承的动态
语言来说,必须能这么做。程序员经常使用这个函数把子类方法的某些任务委托给超
类中适当的方法
注意,我们没有禁止为全部属性赋值,只是禁止为单个小写字母属性赋值,以防与只读属
性 x、y、z 和 t 混淆。
Vector类第4版:散列和快速等值测试
functools.reduce() 可以替换成 sum()
这里的原理
- 它的关键思想是,把一系列值归约成单个值。
- 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。
如:
>>> import functools >>> functools.reduce(lambda a,b: a*b, range(1, 6)) 120
reduce接着用
import functools aa = functools.reduce(lambda a, b: a ^ b, range(1,6)) print(aa) # operator--操作符函数 # https://blog.csdn.net/shengmingqijiquan/article/details/53005129 import operator bb = functools.reduce(operator.xor, range(6)) print(bb)
使用我喜欢的方式编写 Vector.__hash__ 方法,我们要导入 functools 和
operator 模块。(任性的作者)
import functools # ➊ import operator # ➋ class Vector: typecode = 'd' # 排版需要,省略了很多行... 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) # ➎ # 排版需要,省略了很多行...
❹ 创建一个生成器表达式,惰性计算各个分量的散列值。
❺ 把 hashes 提供给 reduce 函数,使用 xor 函数计算聚合的散列值;第三个参数,0 是
初始值(参见下面的警告框)。
eq 方法更有效率
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 # ➍
❷ zip 函数生成一个由元组构成的生成器,元组中的元素来自参数传入的各个可迭代对
象。如果不熟悉 zip 函数,请阅读“出色的 zip 函数”附注栏。前面比较长度的测试是有
必要的,因为一旦有一个输入耗尽,zip 函数会立即停止生成值,而且不发出警告。
使用 zip 和 all 函数实现 Vector.__eq__ 方法
def __eq__(self, other): return len(self) == len(other) and all(a == b for a, b in zip(self, other))
zip 内置函数的使用示例
>>> zip(range(3), 'ABC') # ➊ <zip object at 0x10063ae48> >>> list(zip(range(3), 'ABC')) # ➋ [(0, 'A'), (1, 'B'), (2, 'C')] >>> list(zip(range(3), 'ABC', [0.0, 1.1, 2.2, 3.3])) # ➌ [(0, 'A', 0.0), (1, 'B', 1.1), (2, 'C', 2.2)] >>> from itertools import zip_longest # ➍ >>> 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 有个奇怪的特性:当一个可迭代对象耗尽后,它不发出警告就停止。
❹ itertools.zip_longest 函数的行为有所不同:使用可选的 fillvalue(默认
值为 None)填充缺失的值,因此可以继续产出,直到最长的可迭代对象耗尽。
Vector类第5版:格式化
__format__提供格式化方法,详情和具体代码 page 348
小总结
- repr 如果信息展示过长. 用reprlib 模块可以缩短
2.切片原理
slice(None, 10, 2).indices(5) 负责转换成可用的索引
len 和 _getitem 实现切片的重要方法
- 属性查找失败后,解释器会调用 getattr 方法。利用这个特性,可以搞一些事情
4.reduce 的使用方法
5.zip函数 简单理解矩阵对应