《Fluent Python》学习笔记:第 10 章 序列的修改、散列和切片

本文主要是 Fluent Python 第 10 章的学习笔记。这部分主要是介绍了将 Vector 类一步步升级,实现集合类型常用的特殊方法 __len____getitem__hash____eq____format__ 等。

《Fluent Python》学习笔记:第 10 章 序列的修改、散列和切片

    • 10.1 Vector 类第 1 版:与 Vector2d 类型兼容
    • 10.2 Vector 类第 2 版:可切片的序列
    • 10.3 Vector 类第 3 版:动态存取属性
    • 10.4 Vector 类第 4 版:散列和快速等值测试
    • 10.5 Vector 类第 5 版:格式化
    • 巨人的肩膀

10.1 Vector 类第 1 版:与 Vector2d 类型兼容

实现一个 Vector 类与前一章的 Vector2d 类兼容。

# vctor_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)  # self._components 受保护的实例属性,把 Vector 保存在一个数组中

    def __iter__(self):
        # 定义 __iter__ 方法把Vector实例变成可迭代对象,这样才能拆包
        return iter(self._components)  # iter() 把 self._components 变为迭代器

    def __repr__(self):
        components = reprlib.repr(self._components)  # reprlib.repr()获取有限长长度,如array('d', [0.0, 1.0, 2.0, ...])
        components = components[components.find('['):-1]  # 去掉前面的array('a'和后面的)
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))  # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # 把 typecode 转换为字节序列
                     bytes(self._components))  # 直接用 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))  # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True

    @classmethod  # 装饰器
    def frombytes(cls, octets):  # 类方法第一个参数是 cls
        typecode = chr(octets[0])  # 从第一个字节读取 typecode
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)  # 这里不用拆包了


print(Vector([3.1, 4.2]))
print(Vector((3, 4, 5)))
Vector(range(10))
(3.1, 4.2)
(3.0, 4.0, 5.0)





Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

10.2 Vector 类第 2 版:可切片的序列

Python 的序列协议只需要 __len____getitem__ 两个方法。只要实现了这两个方法,就可以被当做序列。是不是鸭子不重要,只要它表现的像鸭子,我们就把它当做鸭子。这就是鸭子类型(duck typing)。
比如下面这个 FrenchDeck 类,我们说它是序列,因为它实现了序列协议,行为像序列。

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 类也支持序列协议。添加 __len____getitem__ 方法。

from array import array
import reprlib  # 生成长度有限的表示形式
import math


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)  # self._components 受保护的实例属性,把 Vector 保存在一个数组中

    def __iter__(self):
        # 定义 __iter__ 方法把Vector实例变成可迭代对象,这样才能拆包
        return iter(self._components)  # iter() 把 self._components 变为迭代器

    def __repr__(self):
        components = reprlib.repr(self._components)  # reprlib.repr()获取有限长长度,如array('d', [0.0, 1.0, 2.0, ...])
        components = components[components.find('['):-1]  # 去掉前面的array('a'和后面的)
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))  # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # 把 typecode 转换为字节序列
                     bytes(self._components))  # 直接用 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))  # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True

    @classmethod  # 装饰器
    def frombytes(cls, octets):  # 类方法第一个参数是 cls
        typecode = chr(octets[0])  # 从第一个字节读取 typecode
        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])
3
3.0 5.0
array('d', [1.0, 2.0, 3.0])

上面支持了切片,不过不完美,因为 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])  # 神奇的事发生了,如果[]有逗号,那么 __getitem__ 收到的是元组
print(s[1:4:2, 7:9])  # 元组中甚至可以有多个切片对象
print(slice)
print(dir(slice))
1
slice(1, 4, None)
slice(1, 4, 2)
(slice(1, 4, 2), 9)
(slice(1, 4, 2), slice(7, 9, None))

['__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']
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.

indices 属性有很大作用,indices 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和附属索引以及长度超过目标序列的切片。
所以我们可以通过 indices 方法实现能处理切片的 __getitem__ 方法:

from array import array
import reprlib  # 生成长度有限的表示形式
import numbers
import math


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)  # self._components 受保护的实例属性,把 Vector 保存在一个数组中

    def __iter__(self):
        # 定义 __iter__ 方法把Vector实例变成可迭代对象,这样才能拆包
        return iter(self._components)  # iter() 把 self._components 变为迭代器

    def __repr__(self):
        components = reprlib.repr(self._components)  # reprlib.repr()获取有限长长度,如array('d', [0.0, 1.0, 2.0, ...])
        components = components[components.find('['):-1]  # 去掉前面的array('a'和后面的)
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))  # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # 把 typecode 转换为字节序列
                     bytes(self._components))  # 直接用 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))  # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True

    @classmethod  # 装饰器
    def frombytes(cls, octets):  # 类方法第一个参数是 cls
        typecode = chr(octets[0])  # 从第一个字节读取 typecode
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)  # 这里不用拆包了

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

    def __getitem__(self, index):
        cls = type(self)  # 获取实例属性的类,供后面使用
        if isinstance(index, slice):  # 如果 index 参数值是 slice 对象
            return cls(self._components[index])  # 调用类的构造方法,使用 _components 数组的切片构建一个新的 Vector 实例
        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:])
print(v7[1, 2])  # Vector 不支持多维索引,因此索引元组或多个切片会抛出错误
6.0
(1.0, 2.0, 3.0)
(6.0,)



---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

 in 
     60 print(v7[1:4])
     61 print(v7[-1:])
---> 62 print(v7[1, 2])  # Vector 不支持多维索引,因此索引元组或多个切片会抛出错误


 in __getitem__(self, index)
     53         else:
     54             msg = '{cls.__name__} indices must be integers'
---> 55         raise TypeError(msg.format(cls=cls))
     56
     57


TypeError: Vector indices must be integers

10.3 Vector 类第 3 版:动态存取属性

接下来,我们继续升级 Vector 类,使其能够动态存取属性。之前我们通过 @property 装饰器把 x 和 y 标记为了只读属性。我们可以在 Vector 中编写 4 个特性,但是这样太麻烦了。

这时候 __getattr__ 方法就派上用场了。
注意:属性查找失败后,解释器会调用 __getattr__ 方法,简单说就是,my_obj.x 表达式, Python 会检查 my_obj 实例有没有名为 x 的属性,如果没有,就到类(my_obj.__class__)中查找,如果还没有,就顺着继承树查找。如果依旧找不到,调用 my_obj 所属类中定义的 __getattr__ 方法,传入 self 和属性名称的字符串形式(如 ‘x’)。

下面这个例子,我们为 Vector 定义了 __getattr__ 方法,这个方法作用很简单,就是检查所查找的属性是不是 xyzt 中的每个字母,如果是,那么返回对应的分量。

from array import array
import reprlib  # 生成长度有限的表示形式
import numbers
import math


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)  # self._components 受保护的实例属性,把 Vector 保存在一个数组中

    def __iter__(self):
        # 定义 __iter__ 方法把Vector实例变成可迭代对象,这样才能拆包
        return iter(self._components)  # iter() 把 self._components 变为迭代器

    def __repr__(self):
        components = reprlib.repr(self._components)  # reprlib.repr()获取有限长长度,如array('d', [0.0, 1.0, 2.0, ...])
        components = components[components.find('['):-1]  # 去掉前面的array('a'和后面的)
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))  # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # 把 typecode 转换为字节序列
                     bytes(self._components))  # 直接用 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))  # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True

    @classmethod  # 装饰器
    def frombytes(cls, octets):  # 类方法第一个参数是 cls
        typecode = chr(octets[0])  # 从第一个字节读取 typecode
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)  # 这里不用拆包了

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

    def __getitem__(self, index):
        cls = type(self)  # 获取实例属性的类,供后面使用
        if isinstance(index, slice):  # 如果 index 参数值是 slice 对象
            return cls(self._components[index])  # 调用类的构造方法,使用 _components 数组的切片构建一个新的 Vector 实例
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:
            msg = '{cls.__name__} indices must be integers'
        raise TypeError(msg.format(cls=cls))

    shortcut_names = 'xyzt'

    def __getattr__(self, name):
        cls = type(self)  # 获取 Vector 后面待用
        if len(name) == 1:  # 如果属性名只有一个字母,可能是 shrotcut_names 中的一个
            pos = cls.shortcut_names.find(name)  # 查找字母 name 的位置
            if 0 <= pos < len(self._components):  # 如果位置落在范围内,返回数组中对应的元素
                return self._components[pos]
        msg = '{.__name__!r} object has no attribute {!r}'  # 如果测试都失败,抛出 AttributeError,并指明标准的消息文本
        raise AttributeError(msg.format(cls, name))

v = Vector(range(5))
print(v)
print(v.x)  # 获取第一个元素v[0]
v.x = 10  # 为 v.x 赋值,这个操作本应该抛出异常
print(v.x)  # 获取 v.x,得到新值 10
print(v)  # 但是向量的分量没变
(0.0, 1.0, 2.0, 3.0, 4.0)
0.0
10
(0.0, 1.0, 2.0, 3.0, 4.0)

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

另外,__getattr__ 方法的实现也没有考虑 self._components 之外的实例属性,而是从这个属性中获取 shortcut_names 中所列的“虚拟属性”。

为了避免这种前后矛盾问题,我们需要改写 Vector 类中设置属性的逻辑。通过实现 __setattr__ 方法实现。

from array import array
import reprlib  # 生成长度有限的表示形式
import numbers
import math


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)  # self._components 受保护的实例属性,把 Vector 保存在一个数组中

    def __iter__(self):
        # 定义 __iter__ 方法把Vector实例变成可迭代对象,这样才能拆包
        return iter(self._components)  # iter() 把 self._components 变为迭代器

    def __repr__(self):
        components = reprlib.repr(self._components)  # reprlib.repr()获取有限长长度,如array('d', [0.0, 1.0, 2.0, ...])
        components = components[components.find('['):-1]  # 去掉前面的array('a'和后面的)
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))  # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # 把 typecode 转换为字节序列
                     bytes(self._components))  # 直接用 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))  # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True

    @classmethod  # 装饰器
    def frombytes(cls, octets):  # 类方法第一个参数是 cls
        typecode = chr(octets[0])  # 从第一个字节读取 typecode
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)  # 这里不用拆包了

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

    def __getitem__(self, index):
        cls = type(self)  # 获取实例属性的类,供后面使用
        if isinstance(index, slice):  # 如果 index 参数值是 slice 对象
            return cls(self._components[index])  # 调用类的构造方法,使用 _components 数组的切片构建一个新的 Vector 实例
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:
            msg = '{cls.__name__} indices must be integers'
        raise TypeError(msg.format(cls=cls))

    shortcut_names = 'xyzt'

    def __getattr__(self, name):
        cls = type(self)  # 获取 Vector 后面待用
        if len(name) == 1:  # 如果属性名只有一个字母,可能是 shrotcut_names 中的一个
            pos = cls.shortcut_names.find(name)  # 查找字母 name 的位置
            if 0 <= pos < len(self._components):  # 如果位置落在范围内,返回数组中对应的元素
                return self._components[pos]
        msg = '{.__name__!r} object has no attribute {!r}'  # 如果测试都失败,抛出 AttributeError,并指明标准的消息文本
        raise AttributeError(msg.format(cls, name))

    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:  # 特别处理单个字符的属性
            if name in cls.shortcut_names:  # 如果 name 是 xyzt 中的一个,设置特殊错误信息
                error = 'readonly attribute {attr_name!r}'
            elif name.islow():  # 如果 name 是小写字母,为所有小写字母设置一个错误信息
                error = "can't set attributes 'a' to 'z' in {cls_name!r}"
            else:
                error = ''  # 否则把错误消息设为空字符串
            if error:  # 如果有错误消息,抛出 AttributeError
                msg = error.format(cls_name=cls.__name, attr_name=name)
                raise AttributeError(msg)
        super().__setattr__(name, value)  # 默认情况,在超类上调用 __setattr__ 方法,提供标准行为

v = Vector(range(5))
print(v)
print(v.x)  # 获取第一个元素v[0]
# v.x = 10  # 为 v.x 赋值,这里就会报错了
print(v.x)  # 获取 v.x
print(v)  # 但是向量的分量没变
(0.0, 1.0, 2.0, 3.0, 4.0)
0.0
0.0
(0.0, 1.0, 2.0, 3.0, 4.0)

super() 函数用于动态访问超类的方法,程序员经常用这个函数把子类方法的某些任务委托给超类中适当的方法。
注意:类中声明 __slots__ 属性可以防止设置新实例属性,但是不建议使用 __slots__ 是为了避免创建实例属性,而只应该用于节省内存,并且是内存严重不足时才这么做。

建议:多数情况下,如果实现了 __getattr__ 方法,那么也要定义 __setattr__ 方法,以防对象行为不一致。

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

下面,我们要继续升级 Vector 类,使其变成可散列的,因此我们需要实现 __hash__ 方法。

from array import array
import reprlib  # 生成长度有限的表示形式
import numbers
import math
import functools  # 为了使用 reduce 函数
import operator  # 为了使用 xor 函数


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)  # self._components 受保护的实例属性,把 Vector 保存在一个数组中

    def __iter__(self):
        # 定义 __iter__ 方法把Vector实例变成可迭代对象,这样才能拆包
        return iter(self._components)  # iter() 把 self._components 变为迭代器

    def __repr__(self):
        components = reprlib.repr(self._components)  # reprlib.repr()获取有限长长度,如array('d', [0.0, 1.0, 2.0, ...])
        components = components[components.find('['):-1]  # 去掉前面的array('a'和后面的)
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))  # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # 把 typecode 转换为字节序列
                     bytes(self._components))  # 直接用 self._components 构建bytes对象

    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)  # 第三个参数,0 是初始值

    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))  # 不能用hypot方法了,先算各分量的平方和再开方

    def __bool__(self):
        return bool(abs(self))  # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True

    @classmethod  # 装饰器
    def frombytes(cls, octets):  # 类方法第一个参数是 cls
        typecode = chr(octets[0])  # 从第一个字节读取 typecode
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)  # 这里不用拆包了

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

    def __getitem__(self, index):
        cls = type(self)  # 获取实例属性的类,供后面使用
        if isinstance(index, slice):  # 如果 index 参数值是 slice 对象
            return cls(self._components[index])  # 调用类的构造方法,使用 _components 数组的切片构建一个新的 Vector 实例
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:
            msg = '{cls.__name__} indices must be integers'
        raise TypeError(msg.format(cls=cls))

    shortcut_names = 'xyzt'

    def __getattr__(self, name):
        cls = type(self)  # 获取 Vector 后面待用
        if len(name) == 1:  # 如果属性名只有一个字母,可能是 shrotcut_names 中的一个
            pos = cls.shortcut_names.find(name)  # 查找字母 name 的位置
            if 0 <= pos < len(self._components):  # 如果位置落在范围内,返回数组中对应的元素
                return self._components[pos]
        msg = '{.__name__!r} object has no attribute {!r}'  # 如果测试都失败,抛出 AttributeError,并指明标准的消息文本
        raise AttributeError(msg.format(cls, name))

    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:  # 特别处理单个字符的属性
            if name in cls.shortcut_names:  # 如果 name 是 xyzt 中的一个,设置特殊错误信息
                error = 'readonly attribute {attr_name!r}'
            elif name.islow():  # 如果 name 是小写字母,为所有小写字母设置一个错误信息
                error = "can't set attributes 'a' to 'z' in {cls_name!r}"
            else:
                error = ''  # 否则把错误消息设为空字符串
            if error:  # 如果有错误消息,抛出 AttributeError
                msg = error.format(cls_name=cls.__name, attr_name=name)
                raise AttributeError(msg)
        super().__setattr__(name, value)  # 默认情况,在超类上调用 __setattr__ 方法,提供标准行为

v = Vector([1, 2])
print(hash(v))
3

注:使用 reduce 函数时最好提供第三个参数,reduce(function, iterable, initializer)。如果序列为空,initializer 是返回结果;否则,在规约中使用它作为第一个参数,因此应该使用恒等值。比如对 +、| 、 ^来说,initializer 应该是 0,对 *& 来说,应该是 1。
此外我们还是可以把 __hash__ 方法实现为一种映射归约计算。映射过程计算各个分量的散列值,归约过程则使用 xor 运算符聚合所有散列值。如:

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

此外,之前我们实现的 __eq__ 方法其实有一定缺陷,它会认为,Vector([1, 2]) 和 (1, 2) 相等,这里暂时忽略。而且这个实现方式也很低效,因为这个实现方式要完整复制两个操作数,构建两个元组,然后利用元组类型的 __eq__ 方法。但是对于维数很多的向量,效率和内存的问题就会暴露出来。

因此我们改进如下:

def __eq__(self, other):
    if len(self) != len(other):  # 两个对象长度不一样,那么它们不相等
        return False
    # 长度测试很有必要,因为一旦 zip 函数中有一个可迭代对象输入耗尽,则zip函数会立即停止生成值,而且不发出警告
    for a, b in zip(self, other):  # zip 函数生成一个元组构成的生成器
        if a != b:
            return False
        return True

上面这个方法可以进一步优雅化,通过 all() 函数:

def __eq__(self, other):
   return len(self) == len(other) and all(a == b for a, b in zip(self, other))
# 优化后的 Vector 类
from array import array
import reprlib  # 生成长度有限的表示形式
import numbers
import math
import functools  # 为了使用 reduce 函数
import operator  # 为了使用 xor 函数


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)  # self._components 受保护的实例属性,把 Vector 保存在一个数组中

    def __iter__(self):
        # 定义 __iter__ 方法把Vector实例变成可迭代对象,这样才能拆包
        return iter(self._components)  # iter() 把 self._components 变为迭代器

    def __repr__(self):
        components = reprlib.repr(self._components)  # reprlib.repr()获取有限长长度,如array('d', [0.0, 1.0, 2.0, ...])
        components = components[components.find('['):-1]  # 去掉前面的array('a'和后面的)
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))  # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # 把 typecode 转换为字节序列
                     bytes(self._components))  # 直接用 self._components 构建bytes对象

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

    def __hash__(self):
        hashes = (hash(x) for x in self._components)  # 创建一个生成器惰性计算各个分量的散列值
        return functools.reduce(operator.xor, hashes, 0)  # 第三个参数,0 是初始值

    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))  # 不能用hypot方法了,先算各分量的平方和再开方

    def __bool__(self):
        return bool(abs(self))  # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True

    @classmethod  # 装饰器
    def frombytes(cls, octets):  # 类方法第一个参数是 cls
        typecode = chr(octets[0])  # 从第一个字节读取 typecode
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)  # 这里不用拆包了

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

    def __getitem__(self, index):
        cls = type(self)  # 获取实例属性的类,供后面使用
        if isinstance(index, slice):  # 如果 index 参数值是 slice 对象
            return cls(self._components[index])  # 调用类的构造方法,使用 _components 数组的切片构建一个新的 Vector 实例
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:
            msg = '{cls.__name__} indices must be integers'
        raise TypeError(msg.format(cls=cls))

    shortcut_names = 'xyzt'

    def __getattr__(self, name):
        cls = type(self)  # 获取 Vector 后面待用
        if len(name) == 1:  # 如果属性名只有一个字母,可能是 shrotcut_names 中的一个
            pos = cls.shortcut_names.find(name)  # 查找字母 name 的位置
            if 0 <= pos < len(self._components):  # 如果位置落在范围内,返回数组中对应的元素
                return self._components[pos]
        msg = '{.__name__!r} object has no attribute {!r}'  # 如果测试都失败,抛出 AttributeError,并指明标准的消息文本
        raise AttributeError(msg.format(cls, name))

    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:  # 特别处理单个字符的属性
            if name in cls.shortcut_names:  # 如果 name 是 xyzt 中的一个,设置特殊错误信息
                error = 'readonly attribute {attr_name!r}'
            elif name.islow():  # 如果 name 是小写字母,为所有小写字母设置一个错误信息
                error = "can't set attributes 'a' to 'z' in {cls_name!r}"
            else:
                error = ''  # 否则把错误消息设为空字符串
            if error:  # 如果有错误消息,抛出 AttributeError
                msg = error.format(cls_name=cls.__name, attr_name=name)
                raise AttributeError(msg)
        super().__setattr__(name, value)  # 默认情况,在超类上调用 __setattr__ 方法,提供标准行为

v1 = Vector([1, 2])
print(v1)
print(v1 == (1, 2))
(1.0, 2.0)
True

10.5 Vector 类第 5 版:格式化

接下来,我们进行最后一步,实现格式化方法: __format__。此外,这里还实现了计算角坐标的函数 angle(n),以及 angles(),具体的数学原理,忽略,直接看代码:

from array import array
import reprlib  # 生成长度有限的表示形式
import numbers
import math
import functools  # 为了使用 reduce 函数
import operator  # 为了使用 xor 函数
import itertools  # 为了在 __format__ 方法中使用 chain 函数,导入itertools模块


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)  # self._components 受保护的实例属性,把 Vector 保存在一个数组中

    def __iter__(self):
        # 定义 __iter__ 方法把Vector实例变成可迭代对象,这样才能拆包
        return iter(self._components)  # iter() 把 self._components 变为迭代器

    def __repr__(self):
        components = reprlib.repr(self._components)  # reprlib.repr()获取有限长长度,如array('d', [0.0, 1.0, 2.0, ...])
        components = components[components.find('['):-1]  # 去掉前面的array('a'和后面的)
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))  # 从可迭代的 Vector2d 轻松得到一个元组,显示为一个有序对

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # 把 typecode 转换为字节序列
                     bytes(self._components))  # 直接用 self._components 构建bytes对象

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

    def __hash__(self):
        hashes = (hash(x) for x in self._components)  # 创建一个生成器惰性计算各个分量的散列值
        return functools.reduce(operator.xor, hashes, 0)  # 第三个参数,0 是初始值

    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))  # 不能用hypot方法了,先算各分量的平方和再开方

    def __bool__(self):
        return bool(abs(self))  # 使用 abs(self) 计算模,把结果转为布尔值,0.0 为 False,非零值是 True

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

    def __getitem__(self, index):
        cls = type(self)  # 获取实例属性的类,供后面使用
        if isinstance(index, slice):  # 如果 index 参数值是 slice 对象
            return cls(self._components[index])  # 调用类的构造方法,使用 _components 数组的切片构建一个新的 Vector 实例
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:
            msg = '{cls.__name__} indices must be integers'
        raise TypeError(msg.format(cls=cls))

    shortcut_names = 'xyzt'

    def __getattr__(self, name):
        cls = type(self)  # 获取 Vector 后面待用
        if len(name) == 1:  # 如果属性名只有一个字母,可能是 shrotcut_names 中的一个
            pos = cls.shortcut_names.find(name)  # 查找字母 name 的位置
            if 0 <= pos < len(self._components):  # 如果位置落在范围内,返回数组中对应的元素
                return self._components[pos]
        msg = '{.__name__!r} object has no attribute {!r}'  # 如果测试都失败,抛出 AttributeError,并指明标准的消息文本
        raise AttributeError(msg.format(cls, name))

    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:  # 特别处理单个字符的属性
            if name in cls.shortcut_names:  # 如果 name 是 xyzt 中的一个,设置特殊错误信息
                error = 'readonly attribute {attr_name!r}'
            elif name.islow():  # 如果 name 是小写字母,为所有小写字母设置一个错误信息
                error = "can't set attributes 'a' to 'z' in {cls_name!r}"
            else:
                error = ''  # 否则把错误消息设为空字符串
            if error:  # 如果有错误消息,抛出 AttributeError
                msg = error.format(cls_name=cls.__name, attr_name=name)
                raise AttributeError(msg)
        super().__setattr__(name, value)  # 默认情况,在超类上调用 __setattr__ 方法,提供标准行为

    def angle(self, n):  # 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]
            coords = itertools.chain([abs(self)], self.angles())  # 使用chain函数生成生成器表达式,无缝迭代向量的模和各个角坐标
            outer_fmt = '<{}>'  # 使用尖括号显示球面坐标
        else:
            coords = self
            outer_fmt = '({})'  # 使用圆括号显示笛卡尔坐标
        components = (format(c, fmt_spec) for c in coords)  # 创建生成器表达式,按需格式化各个坐标元素
        return outer_fmt.format(', '.join(components))  # 把以逗号分隔的格式化分量插入尖括号或者圆括号中

    @classmethod  # 装饰器
    def frombytes(cls, octets):  # 类方法第一个参数是 cls
        typecode = chr(octets[0])  # 从第一个字节读取 typecode
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)  # 这里不用拆包了


print(Vector([3.1, 4.2]))
print(Vector(range(10)))
v1 = Vector([3, 4])
print(format(v1))
print(format(v1, '.2f'))
print(format(Vector([0, 0, 0]), '.5fh'))
(3.1, 4.2)
(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0)
(3.0, 4.0)
(3.00, 4.00)
<0.00000, 0.00000, 0.00000>

巨人的肩膀

  1. 《Fluent Python》
  2. 《流畅的 Python》

你可能感兴趣的:(《Fluent Python》学习笔记:第 10 章 序列的修改、散列和切片)