流畅的python——10 序列的修改、散列和切片

十、序列的修改、散列和切片

不要检查它是不是鸭子、它的叫声像不像鸭子、它的走路姿势像不像鸭子,等等。具体检查什么取决于你想使用语言的哪些行为。(comp.lang.python,2000 年 7月 26 日) ——Alex Martelli

多维向量

In [20]: from array import array

In [21]: import reprlib

In [22]: import math

In [29]: 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)
    ...:     def __len__(self):
    ...:         return len(self._components)
    ...:     def __getitem__(self, index):  # 支持 [] 和 切片
    ...:         return self._components[index]

我使用 reprlib.repr 的方式需要做些说明。这个函数用于生成大型结构或递归结构的安全表示形式,它会限制输出字符串的长度,用 '...' 表示截断的部分。我希望 Vector实例的表示形式是 Vector([3.0, 4.0, 5.0]) 这样,而不是 Vector(array('d',[3.0, 4.0, 5.0])),因为 Vector 实例中的数组是实现细节。

编写 __repr__ 方法时,本可以使用这个表达式生成简化的 components 显示形式:reprlib.repr(list(self._components))。然而,这么做有点浪费,因为要把self._components 中的每个元素复制到一个列表中,然后使用列表的表示形式。而是直接把 self._components 传给 reprlib.repr 函数,然后去掉 [] 外面的字符。

调用 repr() 函数的目的是调试,因此绝对不能抛出异常。如果 __repr__ 方法的实现有问题,那么必须处理,尽量输出有用的内容,让用户能够识别目标对象。

协议和鸭子类型

在面向对象编程中,协议是非正式的接口,只有在文档中定义,在代码中不定义。任何类只要使用标准的签名和语义实现了协议的方法,就能用在任何期待实现协议功能的地方。其是不是哪个类的子类无关紧要,只要提供了所需的方法即可。

比如:序列协议:只要实现 __len____getitem__ 两个方法。

我们说它是序列,因为它的行为像序列,这才是重点。

协议是非正式的,没有强制力,因此如果你知道类的具体使用场景,通常只需要实现一个协议的部分。例如,为了支持迭代,只需要实现 __getitem__ 方法,没必要实现 __len__ 方法。

支持切片

如果得到的 Vector 对象的切片是一个列表,会丢失很多功能,如果得到是还是一个 vector 对象,就很好了。

切片原理

In [1]: class Y:
   ...:     def __getitem__(self,index):
   ...:         return index
   ...:

In [2]: y = Y()

In [3]: y[1]
Out[3]: 1

In [4]: y[1:2]
Out[4]: slice(1, 2, None)

In [5]: y[1:2:2,9]  # 逗号隔开,index 是元组
Out[5]: (slice(1, 2, 2), 9)

In [6]: y[1:2,2:3]  # 支持多个切片
Out[6]: (slice(1, 2, None), slice(2, 3, None))

indices 方法

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

In [11]: slice(None,11,2).indices(5)
Out[11]: (0, 5, 2)

In [12]: slice(None,None,None).indices(10)
Out[12]: (0, 10, 1)

dir() 和 help(),发现 slice.indices() 方法。这也表明交互式控制台是个有价值的工具,能发现新事物。

In [10]: help(slice.indices)
Help on method_descriptor:

indices(...)
    S.indices(len) -> (start, stop, stride)

    Assuming a sequence of length len, calculate the start and stop
    indices, and the stride length of the extended slice described by
    S. Out of bounds indices are clipped in a manner consistent with the
    handling of normal slices.

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

__getattr__ : obj.x

前四个位置的元素分别为:x, y, z, t

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

测试:

>>> 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  # 优先读取 x 属性,而不是直接调用 __getattr__
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])

仅当对象没有指定名称的属性时,Python 才会调用那个方法,这是一种后备机制。可是,像 v.x = 10 这样赋值之后,v 对象有 x 属性了,因此使用 v.x 获取 x 属性的值时不会调用 __getattr__ 方法了,解释器直接返回绑定到 v.x 上的值,即 10。另一方面,__getattr__ 方法的实现没有考虑到 self._components 之外的实例属性,而是从这个属性中获取 shortcut_names 中所列的“虚拟属性”。

实现 __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():  # 如果 name 是小写字母,为所有小写字母设置一个错误消息。
            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)  # 默认情况:在超类上调用 __setattr__ 方法,提供标准行为。

为了给 AttributeError 选择错误消息,查看了内置的 complex 类型的行为,因为 complex 对象是不可变的

注意,我们没有禁止为全部属性赋值,只是禁止为单个小写字母属性赋值,以防与只读属性 x、y、z 和 t 混淆。

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

__setitem__ 方法,支持 v[0] = 1.1

可散列对象

累积计算

operator 模块以函数的形式提供了 Python 的全部中缀运算符,从而减少使用 lambda 表达式。

>>> n = 0
>>> for i in range(1, 6):
...     n ^= i
>>> import functools
>>> functools.reduce(lambda a, b: a^b, range(6))
>>> import operator
>>> functools.reduce(operator.xor, range(6))

使用 reduce 函数时最好提供第三个参数,reduce(function, iterable, initializer),这样能避免这个异常:TypeError: reduce() of empty sequence with no initial value(这个错误消息很棒,说明了问题,还提供了解决方法)。如果序列为空,initializer 是返回的结果;否则,在归约中使用它作为第一个参数,因此应该使用恒等值。比如,对 +、| 和 ^ 来说, initializer 应该是 0;而对 * 和 & 来说,应该是 1。

from array import array
import reprlib
import math
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)

映射过程计算各个分量的散列值,归约过程则使用 xor 运算符聚合所有散列值。把生成器表达式替换成 map 方法,映射过程更明显:

def __hash__(self):
    hashes = map(hash, self._components)
    return functools.reduce(operator.xor, hashes)

在 Python 2 中使用 map 函数效率低些,因为 map 函数要使用结果构建一个列表。但是在 Python 3 中,map 函数是惰性的,它会创建一个生成器,按需产出结果,因此能节省内存——这与示例 10-12 中使用生成器表达式定义 __hash__ 方法的原理一样。

改进 __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 函数会立即停止生成值,而且不发出警告。

改进:上个改进效率很好,不过用于计算聚合值的整个 for 循环可以替换成一行 all 函数调用:如果所有分量对的比较结果都是 True,那么结果就是 True。只要有一次比较的结果是 False,all 函数就返回 False。zip 也是返回生成器。

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

出色的 zip 函数

使用 for 循环迭代元素不用处理索引变量,还能避免很多缺陷,但是需要一些特殊的实用函数协助。其中一个是内置的 zip 函数。使用 zip 函数能轻松地并行迭代两个或更多可迭代对象,它返回的元组可以拆包成变量,分别对应各个并行输入中的一个元素。

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

输出格式化:__format__

def angle(self, n):  # 计算某个角坐标
    r = math.sqrt(sum(x * x for x in self[n:]))
    a = math.atan2(r, self[n-1])
    if (n == len(self) - 1) and (self[-1] < 0):
        return math.pi * 2 - a
    else:
        return a
def angles(self):  # 计算全部角坐标
    return (self.angle(n) for n in range(1, len(self)))
def __format__(self, fmt_spec=''):
    if fmt_spec.endswith('h'): # 超球面坐标
        fmt_spec = fmt_spec[:-1]
        # 使用 itertools.chain 函数生成生成器表达式,无缝迭代向量的模和各个角坐标。
        coords = itertools.chain([abs(self)],self.angles())
        outer_fmt = '<{}>'
    else:
        coords = self
        outer_fmt = '({})'
    components = (format(c, fmt_spec) for c in coords)
    return outer_fmt.format(', '.join(components))

协议,是鸭子类型语言使用的非正式接口。

我们经常分析 Python 标准对象的行为,然后进行模仿,让 Vector 的行为符合 Python 风格。

模仿内置类型实现类时,记住一点:模仿的程度对建模的对象来说合理即可。例如,有些序列可能只需要获取单个元素,而不必提取切片。 ——Python 语言参考手册中“Data Model”一章

不要为了满足过度设计的接口契约和让编译器开心,而去实现不需要的方法,我们要遵守 KISS 原则(http://en.wikipedia.org/wiki/KISS_principle)。

“地道”并不是指使用最鲜为人知 的语言特性。

python 风格的求和方式

我喜欢 Evan Simpson 的代码,不过也喜欢 David Eppstein 对此给出的评论:

如果你想计算列表中各个元素的和,写出的代码应该看起来像是在“计算元素之和”,而不是“迭代元素,维护一个变量 t,再执行一系列求和操作”。如果不能站在一定高度上表明意图,让语言去关注低层操作,那么要高级语言干嘛?

随后,Alex 建议提供并实现了 sum() 函数。这次讨论之后三个月,Python 2.3 就内置了这个函数。因此,Alex 喜欢的句法变成了标准:

>>> sum([sub[1] for sub in my_list])
60

下一年年末(2004 年 11 月),Python 2.4 发布了,这一版引入了生成器表达式。因此,在我看来,Guy Middleton 那个问题目前最符合 Python 风格的答案是:

>>> sum(sub[1] for sub in my_list)
60
posted @ 2021-09-27 19:13  pythoner_wl  阅读(96)  评论(0编辑  收藏  举报