当前位置: 代码迷 >> 综合 >> Fluent Python - Part10 序列的修改,散列和切片
  详细解决方案

Fluent Python - Part10 序列的修改,散列和切片

热度:51   发布时间:2024-02-28 01:57:25.0

本章以第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, stopstride 都变成非负数,而且都落在指定长度序列的边界内。

在 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__ 方法的实现。

接下来我们将介绍“接口:从协议到抽象基类”