《流畅的Python》读书笔记12(第十章:序列的修改、散列和切片)

133 阅读6分钟

本章将以第9章定义的二维向量Vector2d类为基础,定义表示多维向量的Vector类。这个类的行为与Python中的标准的不可变扁平序列一样。

10.1 Vector类:用户定义的序列类型

我们将使用组合模式实现Vector类,而不使用继承。向量的分量存储在浮点数数组中,而且还将实现不可变、扁平序列所需的方法。

10.2 Vector第一版:与Vector2d兼容

示例10-2 vector_v1.py:从vector2d_v1.py衍生而来

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

示例10-2 测试Vector.__init__和Vector.__repr__方法

Vector([3.1, 4.2])
Vector([3.1, 4.2])
Vector((3,4,5))
Vector([3.0, 4.0, 5.0])
Vector(range(10))
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

10.3 协议和鸭子类型

在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。例如,Python的序列协议只需要__len__和__getitem__两个方法。只要使用标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。

10.4 Vector类第二版:可切片的序列

如果能委托给对象中的序列属性(如self._components数组),支持序列协议特别简单。

class Vector:
# ...

    def __len__(self):
        return len(self._components)

    def __getitem__(self, index):
        return self._components[index]

添加这两个方法后,就能执行下述操作了:

v1 = Vector([3, 4, 5])
len(v1)
3
v1[0], v1[-1]
(3.0, 5.0)
v7 = Vector(range(7))
v7[1:4]
array('d', [1.0, 2.0, 3.0])

现在连切片都支持了,不过尚不完美。如果Vector实例的切片也是Vector实例,而不是数组,那就更好了。

想想内置的序列类型,切片得到的都是各自类型的新实例,而不是其他类型。

10.4.1 切片原理

一例胜千言 (A demo is worth a thousand words) 示例10-4 了解__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] # 如果[]中有逗号,那么\_\_getitem__收到的是元组

(slice(1, 4, 2), 9)

>>> s[1:4:2, 7:9]

(slice(1, 4, 2), slice(7, 9, None))

>>>

示例10-5 查看slice类的属性

>>> slice

<class 'slice'>

>>> 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,发现有个indices属性,这个方法有很大的作用。help(slice.indices)给出如下信息:

image.png

给定长度为len的序列,计算S表示的扩展切片的起始(start)和结尾(stop)索引,以及步幅(stride).超出边界的索引会被截掉。

换句话说,indices方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法会"整顿"元组,把start、stop和stride都变成非负数,而且都落在指定长度序列的边界内。

下面举几个例子,假设有个长度为5的序列,例如'ABCDE':

>>> slice(None, 10, 2).indices(5)

(0, 5, 2)

>>> slice(-3, None, None).indices(5)

(2, 5, 1)

10.4.2 能处理切片的__getitem__方法

示例10-6 改造__getitem__方法


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

示例10-7 测试改进后的Vector.__getitem__

v7 = Vector(range(7))
v7[-1]
6.0
v7[1:4]
Vector([1.0, 2.0, 3.0])
v7[-1:]
Vector([6.0])
v7[1,2]
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  ...
    raise TypeError(msg.format(cls=cls))
TypeError: Vector indices must be integers

10.5 Vector第3版:动态存取属性

Vector2d变成Vector之后,就没办法通过名称访问向量的分量了(如v.x和v.y)。现在我们处理的向量可能有大量分量。不过,若能通过单个字母访问前几个分量的话会比较方便。比如,用x, y和z代替v[0]、v[1]和v[2]。

我们想额外提供下述句法,用于读取向量的前四个分量:

v = Vector(range(10))
v.x
0.0
v.y, v.z, v.t
(1.0, 2.0, 3.0)

我们可以在Vector中编写四个特性,但这样太麻烦。特殊方法__getattr__提供了更好的方式。

属性查找失败后,解释器会调用__getattr__方法。对于my_obj.x表达式,Python会检查my_obj实例有没有名为x的属性,如果没有, 到类(my_obj.__class__)中查找;如果还没有,顺着继承树继续查找。如果依旧找不到,调用my_obj所属类中定义的__getattr__方法,传入self和属性名称的字符串形式。

示例10-8 添加__getattr__方法

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

__getattr__方法的实现不难,但是这样实现还不够。

示例10-9 不恰当的行为:为v.x赋值没有抛出错误,但是前后矛盾

>>> v = Vector(range(5))
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
>>> v.x
0.0
>>> v.x = 10 #为v.x赋新值。这个操作应该抛出异常
>>> v.x
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])

你能解释为什么会这样吗?具体而言,如果向量的分量数组中没有新值,为什么v.x返回10?

示例10-9之所以前后矛盾,是__getattr__的运作方式导致的:仅当对象没有指定名称的属性时,Python才会调用__getattr__方法,这是一种后备机制。可是,像v.x=10这样赋值之后,v对象有x属性了,因此使用v.x获取x属性的值时不会调用__getattr__方法,解释器直接返回绑定到v.x上的值,即10.

为了避免这种矛盾的现象,我们哟啊改写Vector类中设置属性的逻辑。

示例10-10 在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)

多数时候,如果实现了__getattr__方法,那么也要定义__setattr__方法,以防对象的行为不一致。

10.6 Vector类第4版:散列和快速等值测试

我们要再次实现__hash__方法。加上现有的__eq__方法,这会把Vector实例变成可散列的对象。

这次,我们要使用^(异或)运算符一次计算各个分值的散列值,像这样:v[0]^v[1]^v[2]...。这正是functools.reduce函数的作用。

image.png 图10-1:归约函数(reduce、sum、any、all)把序列或有限的可迭代对象变成一个聚合结果

示例10-11 计算整数0~5的累计异或的3种方式:

>>> n = 0

>>> for i in range(1,6):

...     n ^= i

... 

>>> n

1

>>> import functools

>>> functools.reduce(lambda a,b:a^b, range(6))

1

>>> import operator

>>> functools.reduce(operator.xor, range(6))

1
>>>

示例10-12 添加__hash__方法

import functools
import operator
import numbers
from array import array
import reprlib
import math


class Vector:
    ...

    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)

示例10-13 为了提高效率,Vector.__eq__方法在for循环中使用zip函数

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

示例10-14 更简洁的写法

def __eq__(self, other):
    return len(self) == len(other) and all(a == b for a, b in zip(self, other))

zip函数的名字取自拉链系结物(zipper fastener),因为这个物品用于把两个拉链边的链牙咬合到一起,这形象地说明了zip(left,right)的作用

示例10-15 zip内置函数的使用示例

>>> zip(range(3), 'ABC')

<zip object at 0x7fba80034a48>

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