本章以第 9 章定义的二维向量 Vector2d 类为基础,向前迈出一大步,定义表示多维向量的 Vector 类。这个类的行为与 Python 标准中的不可变扁平序列一样。Vector 实例中的元素是浮点数,本章结束后 Vector2d 类将支持以下功能

  • 基本的序列协议 -- __len____getitem__
  • 正确表述拥有很多元素的实例
  • 适当的切片支持,用于生成新的 Vector 实例
  • 综合各个元素的值计算散列值
  • 自定义的格式语言扩展

此外,我们还将通过 __getattr__ 方法实现属性的动态存取,以此取代 Vector2d 使用的只读属性 -- 不过,序列类型通常不会这么做

在大量代码之间,我们将穿插讨论一个概念:把协议当做正式借口。我们将说明协议和鸭子类型之间的关系,以及对自定义类型的影响

Vector 第一版:与 Vector2d 类兼容

Vector 类要尽量与上一章的 Vector2d 类兼容。为了编写 Vector(3, 4),Vector(3, 4, 5) 这样的代码,我们可以让 __init__ 方法接受任意个参数(通过 *args);但是,序列类型的构造方法最好接受可迭代的对象为参数,因为所有内置的序列类型都是这样做的。下面是我们的第一版 Vector 代码


In [14]:
from array import array
import reprlib
import math

class Vector:
    typecode = 'd'
    
    def __init__(self, components):
        self._components = array(self.typecode, components) # 把 Vector 分量保存到一个数组中('d' 表示 double)
    
    def __iter__(self):
        return iter(self._components)
    
    def __repr__(self):
        # 使用 reprlib.repr() 函数获取 self._commponents 有限长度表示,如 array('d', [0.0, 1.0, 2.0, 3.0, 4.0, ...])
        components = reprlib.repr(self._components) 
        components = components[components.find('['):-1] # 将字符串插入构造方法调用之前,去掉前面的 'd' 和后面的 )
        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) #我们只需要改动一行,直接把 memoryview 传给构造方法,不用使用 * 拆包

In [15]:
Vector([3.1, 4.2])


Out[15]:
Vector([3.1, 4.2])

In [16]:
Vector((3, 4, 5))


Out[16]:
Vector([3.0, 4.0, 5.0])

In [17]:
Vector(range(10)) # reprlib.repr 限制了长度


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

我们使用 reprlib.repr 的方式需要做些说明。这个函数用于生成大型结构或递归结构的安全表示形式,它会限制输出字符串的长度,用 '...' 表示截断的部分。另外我们希望 Vector 实例的表现形式是 Vector([3.0, 4.0, 5.0]),而不是 Vector(array('d', [3.0, 4.0, 5.0])),因为 Vector 实例中的数组是实现细节。因为这两种构造方法调用方式所构建的 Vector 对象是一样的,所以我选择使用更简单的语法,即传入列表参数

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

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

注意,__str__, __eq____bool__ 方法与 Vector2d 类中的一样,而 frombytes 方法也只是把 * 去掉了。这是 Vector2d 可迭代的好处之一

顺便说一下,我们本可以让 Vector 继承 Vector2d,但是没有这么做,原因有两点,一是两个构造方法不兼容,所以不建议继承,这一点可以通过适当处理 __init__ 方法解决,第二个原因更重要:我想把 Vector 类作为单独的示例,因此实现序列协议,接下来我们讨论 协议 这个术语,然后实现序列协议

协议和鸭子类型

在第一章我们就说过,Python 中创建功能完善的序列类型无需使用继承,只需实现符合序列协议的方法

在面向对象中,协议是非正式的接口,只在文档中定义,在代码中不定义。例如,Python 的序列协议只需要 __len____getitem__ 两个方法。任何类(如 Spam)只要使用标准签名和语义实现了这两个方法,就能用在任何期待序列的地方。Spam 是不是哪个类的子类无关紧要,只要提供了所需方法即可。第一章见过一个例子,下面再次给出代码:


In [18]:
import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamons 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]

FrenchDeck 类能充分利用 Python 的很多功能,因为它实现了序列协议,不过代码中并没有声明这一点。任何有经验的 Python 程序员只要看一眼就知道它是序列,即便它是 Object 的子类也无妨。我们说它是序列,因为它的行为像序列,这才是重点

Alex Martelli 说不要检查它是不是鸭子,而是看它的叫声像不像鸭子,走路姿势像不像鸭子,等等,这样的类人称鸭子类型

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

下面,我们将在 Vector 类中实现序列协议。我们先不支持完美的切片,稍后再完善

Vector 类第 2 版:可切片序列

如 FrenchDeck 类所示,如果能委托给对象中的序列属性(如 self._component 数组),支持序列协议特别简单。下面只有一行代码的 __len____getitem__ 方法是个好的开始


In [19]:
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) 
    
    # 上面都一样
    def __len__(self):
        return len(self._components)
    
    def __getitem__(self, index):
        return self._components[index]

In [22]:
v1 = Vector([3, 4, 5])
len(v1)


Out[22]:
3

In [23]:
v1[0], v1[1]


Out[23]:
(3.0, 4.0)

In [24]:
v7 = Vector(range(7))
v7[1: 4]


Out[24]:
array('d', [1.0, 2.0, 3.0])

可以看到,现在连切片都支持了,不过不太完美,如果 Vector 实例的切片也是 Vector 实例,而不是数组,那就好了。前面那个 FrenchDeck 也有类似的问题:切片得到的是列表。对 Vector 类来说,如果切片生成普通的数组,将缺失大量功能

想想内置的序列类型,切片得到的都是各自类型的实例,而不是其他类型。为了将 Vector 的实例的切片也变成 Vector 实例,我们不能简单的将切片交给数组切片做,我们要分析传给 __getitem__ 方法的参数,做适当的处理

下面看看 Python 如何把 my_seq[1:3] 语法变成 my_seq.__getitem__(...) 的参数


In [26]:
class MySeq:
    def __getitem__(self, index):
        return index
    
s = MySeq()
s[1]


Out[26]:
1

In [27]:
s[1:4]


Out[27]:
slice(1, 4, None)

In [28]:
s[1:4:2]


Out[28]:
slice(1, 4, 2)

In [29]:
s[1:4:2, 9] # [ ] 中如果有逗号,__getitem__ 收到的是元组


Out[29]:
(slice(1, 4, 2), 9)

In [30]:
s[1:4:2, 7:9] # 元组中甚至有多个切片对象


Out[30]:
(slice(1, 4, 2), slice(7, 9, None))

现在,我们来看看 slice 本身:


In [31]:
slice # slice 是内置的类型


Out[31]:
slice

In [32]:
dir(slice) # 有 start, stop, step 数据属性,以及 indices 方法


Out[32]:
['__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']

上面的 indices 属性非常有用,但是鲜为人知。


In [33]:
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.

给定长度为 len 的序列,计算 S 表示的扩展切片的起始(start)和结尾(stop)索引,以及步幅(step)。超过边界的索引会被截掉,这与常规切片处理方式一样

换句话说,indices 方法开放了内置序列的棘手逻辑,用于优雅的处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法会 “整顿” 元组,把 start,stop,stride 都变成非负数,而且都落在边界內,下面举个例子,例如有个长度为 5 的序列:


In [37]:
slice(None, 10, 2).indices(5)


Out[37]:
(0, 5, 2)

In [38]:
slice(-3, None, None).indices(5)


Out[38]:
(2, 5, 1)

在 Vector 类中无需使用 slice.indices() 方法,因为收到切片参数时,我们会委托 _components 数组处理。但是,如果你没有底层序列类型作为依靠,那么使用这个方法能节省大量时间

现在我们知道如何处理切片了,来看看 Vector.__getitem__ 方法改进后的实现

Vector 第 2 版:能处理切片的 __getitem__ 方法


In [42]:
from array import array
import reprlib
import math
import numbers

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):
        cls = type(self)                # 获取实例所属的类
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral): # index 是 int 或其他整数类型
            return self._components[index]
        else:
            msg = '{cls.__name__} indices must be integers'
            raise TypeError(msg.format(cls=cls))

大量使用 isinstance 可能表明面向对象设计的不好,不过在 __getitem__ 方法中使用它处理切片是合理的。注意上面例子中用的是 numbers.Integer,这是一个抽象基类(Abstract Base Class,ABC)。在 isinstance 中使用抽象基类做测试能让 API 更灵活且容易更新,原因在下章讲。可惜,Python 3.4 标准库没有 slice 抽象基类

这个异常 TypeError 也是从字符串切片学得,字符串切片报错就会抛出 TypeError,返回的错误消息也是抄的(= =),为了创建符合 Python 风格的对象,我们要模仿 Python 内置的对象


In [43]:
v7 = Vector(range(7))
v7[-1]


Out[43]:
6.0

In [44]:
v7[1:4] # 看到现在行为正确了


Out[44]:
Vector([1.0, 2.0, 3.0])

In [45]:
v7[-1:]


Out[45]:
Vector([6.0])

In [46]:
v7[1, 2] # Vector 不支持多维索引,所以索引元组或多个切片会抛出错误


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-46-fcbe61a650f9> in <module>()
----> 1 v7[1, 2]

<ipython-input-42-68f990aa90ca> in __getitem__(self, index)
     52         else:
     53             msg = '{cls.__name__} indices must be integers'
---> 54             raise TypeError(msg.format(cls=cls))

TypeError: Vector indices must be integers

Vector 类第 3 版:动态存储属性

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

在 Vector2d 中,我们使用 @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 中的某个字段,是则返回相应分量


In [47]:
from array import array
import reprlib
import math
import numbers

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):
        cls = type(self)                # 获取实例所属的类
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral): # index 是 int 或其他整数类型
            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)

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

In [48]:
v = Vector(range(5))
v


Out[48]:
Vector([0.0, 1.0, 2.0, 3.0, 4.0])

In [49]:
v.x


Out[49]:
0.0

In [51]:
v.x = 10
v.x  # 读取到新的值


Out[51]:
10

In [52]:
v # 向量的分量却没有变


Out[52]:
Vector([0.0, 1.0, 2.0, 3.0, 4.0])

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

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

回想前一章最后一个 Vector2d 实例中,如果为 .x 或 .y 实例属性赋值,会抛出 AttributeError,为了避免歧义,在 Vector 类中,如果为名称是单个小写字母属性赋值,我们也想抛出那个异常。为此,我们要实现 __setattr__ 方法,如下所示:


In [53]:
from array import array
import reprlib
import math
import numbers

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):
        cls = type(self)                # 获取实例所属的类
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral): # index 是 int 或其他整数类型
            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)

        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:
            if name in cls.shortcut_names:
                error = 'readonly attribute {attr_name!}'
            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) # 默认情况,调用超类的 __setattr__ 方法,提供标准行为

super 函数用于动态访问超类的方法,对 Python 这样支持多重继承的动态语言来说,必须能这么做,程序员经常使用这个函数把子类方法的某些任务委托给超类中适当的方法,如上面例子所示,在第 12 章我们会继续探索 super() 方法

为了给 AttributeError 选择错误消息,作者查看了 complex 类型的行为,当试图修改此类的只读属性会抛出 AttributeError,并且错误消息为 "can't set attribute",我们的错误消息参考了它

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

我们知道,如果在类中声明 __slots__ 属性可以放之新实例属性,但是在这里没有这么做,因为 __slots__ 应该是你在内存严重不足时候使用的,不要滥用。

虽然这个示例不支持为 Vector 分量赋值,但是有一个问题要特别注意,多数时候,如果实现了 __getattr__ 方法,那么也要定义 __setattr__ 方法,防止对象的行为不一致

如果想允许修改分量,可以使用 __setitime__ 方法,支持 v[0] = 1.1 这样的赋值,以及(或者)实现 __setattr__ 方法,支持 v.x = 1.1 这样的赋值。不过,我们要保持 Vector 是不可变的,因为下一节中,我们将它变成可散列的。

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

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

之前的 __hash__ 方法简单地计算 hash(self.x) ^ hash(self.y)。这一次,我们要使用异或运算符以此计算各个分量的散列值,像这样:v[0] ^ v[1] ^ v[2] ...。我们有如下几种方法可以较为方便的完成这个功能:


In [2]:
n = 0
for i in range(1, 6): n ^= i
n


Out[2]:
1

In [3]:
import functools
functools.reduce(lambda a, b: a ^ b, range(6))


Out[3]:
1

In [4]:
import operator
functools.reduce(operator.xor, range(6))


Out[4]:
1

这三种方法中,我最喜欢最后一种,其次是 for 循环

为了用自己喜欢的方式计算 hash 值,我们引入了 functools 和 operator 模块,编写 __hash__ 方法,如下所示:


In [7]:
from array import array
import reprlib
import math
import numbers
import functools
import operator

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):
        cls = type(self)                # 获取实例所属的类
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral): # index 是 int 或其他整数类型
            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)

        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:
            if name in cls.shortcut_names:
                error = 'readonly attribute {attr_name!}'
            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) # 默认情况,调用超类的 __setattr__ 方法,提供标准行为
        
    def __hash__(self):
        hashs = (hash(x) for x in self._components) # 注意这里是生成器表达式,不是列表推导式,可以节省内存
        return functools.reduce(operator.xor, hashs)

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

上面实现的 __hash__ 是一种规约映射计算:把函数应用到各个元素上,生成一个新的序列(映射,map),然后计算聚合值(规约,reduce)

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


In [8]:
def __hash__(self):
    hashs = map(hash, self._components)
    return functools.reduce(operator.xor, hashes)

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

既然讲到了规约函数,那就把前面草草实现的 __eq__ 方法修改一下,减少处理时间和内存用量 -- 至少对大型向量来说是这样。前面的 __eq__ 方法实现的非常简洁:

def __eq__(self, other):
    return tuple(self) == tuple(other)

Vector2d 和 Vector 都可以这样做,它甚至还会认为 Vector([1, 2]) 和 (1, 2) 相等。这或许是个问题,我们这里先忽略,可是对于有几千个分量的 Vector 实例来说,效率十分低下。上述实现方式都要完整复制两个操作数,构建元组,而这么做只是为了用 tuple 类型的 __eq__ 方法。对 Vector2d(只有两个分量) 来说,这是个捷径,但是对于维数很多的向量来说就不同了。下面比较两个 Vector 实例(或者比较一个 Vector 和一个可迭代对象)的方式更好。


In [9]:
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 函数会立即停止生成值,而不发生警告

上面的代码效率很好,不过计算鞠和值的 for 循环可以替换成一行 all 函数调用:如果所有分量的比较结果都是 True,结果是 True。只要有一次结果是 False,all 函数就返回 False。使用 all 函数实现的 __eq__ 方法如下所示:


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

下面是 zip 函数的一些使用示例:


In [12]:
zip(range(3), 'ABC')


Out[12]:
<zip at 0x7f77e42d3b48>

In [13]:
list(zip(range(3), 'ABC'))


Out[13]:
[(0, 'A'), (1, 'B'), (2, 'C')]

In [14]:
list(zip(range(3), 'ABC', [0.0, 1.0, 2.0, 3.0])) # 当一个可迭代对象耗尽后,它不发出警告就停止


Out[14]:
[(0, 'A', 0.0), (1, 'B', 1.0), (2, 'C', 2.0)]

In [16]:
from itertools import zip_longest
# zip_longest 使用可选的 fillvalue(默认是 None) 填充缺失的值,直到最长的可迭代对象耗尽
list(zip_longest(range(3), 'ABC', [0.0, 1.0, 2.0, 3.0]))


Out[16]:
[(0, 'A', 0.0), (1, 'B', 1.0), (2, 'C', 2.0), (None, None, 3.0)]

为了避免在 for 循环手动处理索引变量,还经常使用内置的 enumerate 生成器函数,在第 14 章介绍

Vector 类第 5 版:格式化

Vector 类的 __format__ 方法与 Vector2d 类相似,但是不使用极坐标,而是用球面坐标,因为 Vector 类支持 n 个维度,而超过 4 维之后,球体变成了 ”超球体“。因此我们把自定义格式后缀从 'p' 改成了 'h'

例如,对 4 维空间(len(v) == 4) 中的 Vector 对象来说,'h' 代码得到的结果是这样:<r, th1, th2, th3>。其中,r 是模,余下 3 个是角坐标

在小幅度改动 __format__ 方法之前,我们要定义两个辅助方法,一个是 angle(n)。用于计算某个角坐标,另一个是 angles(),返回所有角坐标构成的可迭代对象,我们不会讲解其中的数学原理,好奇可以上 wiki 查。

下面是完整代码:


In [1]:
from array import array
import reprlib
import math
import numbers
import functools
import operator
import itertools

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 len(self) == len(other) and all(a == b for a, b in zip(self, 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):
        cls = type(self)                # 获取实例所属的类
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral): # index 是 int 或其他整数类型
            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)

        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:
            if name in cls.shortcut_names:
                error = 'readonly attribute {attr_name!}'
            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) # 默认情况,调用超类的 __setattr__ 方法,提供标准行为
        
    def __hash__(self):
        hashs = (hash(x) for x in self._components) # 注意这里是生成器表达式,不是列表推导式,可以节省内存
        return functools.reduce(operator.xor, hashs)

    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]
            coords = itertools.chain([abs(self)], # 使用 chain 函数生成生成器表达式,无缝迭代向量的模和各个角坐标
                                     self.angles())
            outer_fmt = '<{}>' # 球面坐标
            
        else:
            coords = self
            outer_fmt = '({})' # 笛卡尔坐标
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(', '.join(components))

注意 itertools.chain 函数

hain(iter1, iter2, ..., iterN):

给出一组迭代器(iter1, iter2, ..., iterN),此函数创建一个新迭代器来将所有的迭代器链接起来,返回的迭代器从iter1开始生成项,知道iter1被用完,然后从iter2生成项,这一过程会持续到iterN中所有的项都被用完。下面是一个简单的例子:


In [2]:
test = itertools.chain('abc', 'de', 'f')
test


Out[2]:
<itertools.chain at 0x7f4ab04f4b00>

In [3]:
for i in test:
    print(i)


a
b
c
d
e
f

下面是一些 Vector 类的测试:


In [30]:
Vector([3.1, 4.2])


Out[30]:
Vector([3.1, 4.2])

In [31]:
Vector([3.0, 4.0, 5.0])


Out[31]:
Vector([3.0, 4.0, 5.0])

In [32]:
Vector(range(10))


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

In [33]:
v1 = Vector([3, 4])
x, y = v1
x, y


Out[33]:
(3.0, 4.0)

In [34]:
v1


Out[34]:
Vector([3.0, 4.0])

In [35]:
v1_clone = eval(repr(v1))
v1 == v1_clone


Out[35]:
True

In [36]:
print(v1)


(3.0, 4.0)

In [38]:
octets = bytes(v1)
octets


Out[38]:
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'

In [39]:
abs(v1)


Out[39]:
5.0

In [40]:
bool(v1), bool(Vector([0, 0]))


Out[40]:
(True, False)

In [42]:
v1_clone = Vector.frombytes(bytes(v1))
v1 == v1_clone


Out[42]:
True

In [43]:
v1 = Vector([3, 4, 5])
x, y, z = v1
x, y, z


Out[43]:
(3.0, 4.0, 5.0)

In [44]:
v1


Out[44]:
Vector([3.0, 4.0, 5.0])

In [45]:
v1_clone = eval(repr(v1))
v1_clone == v1


Out[45]:
True

In [46]:
print(v1)


(3.0, 4.0, 5.0)

In [47]:
abs(v1)


Out[47]:
7.0710678118654755

In [48]:
bool(v1), bool(Vector([0, 0, 0]))


Out[48]:
(True, False)

In [49]:
v7 = Vector(range(7))
v7


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

In [50]:
abs(v7)


Out[50]:
9.539392014169456

In [51]:
v1 = Vector([3, 4, 5])
v1_clone = Vector.frombytes(bytes(v1))
v1 == v1_clone


Out[51]:
True

In [52]:
v1 = Vector([3, 4, 5])
len(v1)


Out[52]:
3

In [53]:
v1[0], v1[len(v1)-1], v1[-1]


Out[53]:
(3.0, 5.0, 5.0)

In [54]:
v7 = Vector(range(7))
v7[-1]


Out[54]:
6.0

In [55]:
v7[1:4]


Out[55]:
Vector([1.0, 2.0, 3.0])

In [56]:
v7[-1:]


Out[56]:
Vector([6.0])

In [57]:
v7[1,2]


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-57-82fccfd8982a> in <module>()
----> 1 v7[1,2]

<ipython-input-29-19bd5986e4d6> in __getitem__(self, index)
     55         else:
     56             msg = '{cls.__name__} indices must be integers'
---> 57             raise TypeError(msg.format(cls=cls))
     58 
     59     shortcut_names = 'xyzt'

TypeError: Vector indices must be integers

In [59]:
v7.x


Out[59]:
0.0

In [60]:
v7.y, v7.z, v7.t


Out[60]:
(1.0, 2.0, 3.0)

In [61]:
v7.k


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-61-0ec6231d4933> in <module>()
----> 1 v7.k

<ipython-input-29-19bd5986e4d6> in __getattr__(self, name)
     67                 return self._components[pos]
     68         msg = '{.__name__!r} object has no attribute {!r}'
---> 69         raise AttributeError(msg.format(cls, name))
     70 
     71     def __setattr__(self, name, value):

AttributeError: 'Vector' object has no attribute 'k'

In [62]:
v3 = Vector(range(3))
v3.t


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-62-7eef93f5b5f0> in <module>()
      1 v3 = Vector(range(3))
----> 2 v3.t

<ipython-input-29-19bd5986e4d6> in __getattr__(self, name)
     67                 return self._components[pos]
     68         msg = '{.__name__!r} object has no attribute {!r}'
---> 69         raise AttributeError(msg.format(cls, name))
     70 
     71     def __setattr__(self, name, value):

AttributeError: 'Vector' object has no attribute 't'

In [63]:
v3.spam


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-63-5f03e49da060> in <module>()
----> 1 v3.spam

<ipython-input-29-19bd5986e4d6> in __getattr__(self, name)
     67                 return self._components[pos]
     68         msg = '{.__name__!r} object has no attribute {!r}'
---> 69         raise AttributeError(msg.format(cls, name))
     70 
     71     def __setattr__(self, name, value):

AttributeError: 'Vector' object has no attribute 'spam'

In [64]:
v1 = Vector([3, 4])
v2 = Vector([3.1, 4.2])
v3 = Vector([3, 4, 5])
v6 = Vector(range(6))
hash(v1), hash(v3), hash(v6)


Out[64]:
(7, 2, 1)

In [66]:
import sys
hash(v2) == (384307168202284039 if sys.maxsize > 2 ** 32 else 357915986)


Out[66]:
True

In [67]:
v1 = Vector([3, 4])
format(v1)


Out[67]:
'(3.0, 4.0)'

In [68]:
format(v1, '.2f')


Out[68]:
'(3.00, 4.00)'

In [69]:
format(v1, '.3e')


Out[69]:
'(3.000e+00, 4.000e+00)'

In [70]:
v3 = Vector([3, 4, 5])
format(v3)


Out[70]:
'(3.0, 4.0, 5.0)'

In [71]:
format(Vector(range(7)))


Out[71]:
'(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'

In [5]:
format(Vector([1, 1]), 'h')


Out[5]:
'<1.4142135623730951, 0.7853981633974483>'

In [6]:
format(Vector([1, 1]), '.3eh')


Out[6]:
'<1.414e+00, 7.854e-01>'

In [8]:
format(Vector([1, 1]), '0.5fh')


Out[8]:
'<1.41421, 0.78540>'

In [9]:
format(Vector([1, 1, 1]), 'h')


Out[9]:
'<1.7320508075688772, 0.9553166181245093, 0.7853981633974483>'

In [10]:
format(Vector([2, 2, 2]), '.3eh')


Out[10]:
'<3.464e+00, 9.553e-01, 7.854e-01>'

In [11]:
format(Vector([0, 0, 0]), '0.5fh')


Out[11]:
'<0.00000, 0.00000, 0.00000>'

In [12]:
format(Vector([-1, -1, -1, -1]), 'h')


Out[12]:
'<2.0, 2.0943951023931957, 2.186276035465284, 3.9269908169872414>'

In [13]:
format(Vector([2, 2, 2, 2]), '.3eh')


Out[13]:
'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'

In [14]:
format(Vector([0, 1, 0, 0]), '.05fh')


Out[14]:
'<1.00000, 1.57080, 0.00000, 0.00000>'