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

本文详细介绍了如何基于Python构建一个表示多维向量的Vector类,该类实现了基本的序列协议,包括__len__和__getitem__方法,支持切片操作并返回新的Vector实例。此外,通过__getattr__方法实现了动态属性存取,允许通过单个字母访问向量分量。文章还探讨了协议和鸭子类型的关联,并提供了逐步完善Vector类的代码实现。
摘要由CSDN通过智能技术生成

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

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

此外,我们还将通过 __getattr__ 方法实现属性的动态存取,以此取代 Vector2d 使用的只读特性。

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

Vector类第一版:与Vector2d类兼容

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) # 不用解包

协议和鸭子类型

在 Python 中创建功能完善的序列类型无需使用继承,只需实现符合序列协议的方法。不过,这里说的协议是什么呢?

协议是非正式的接口,只在文档中定义,在代码中不定义。例如,Python 的序列协议只需要 __len____getitem__ 两个方法。任何类(如Spam), 只需使用标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。

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]

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 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 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法会整顿元祖,把 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 False
    for a, b in zip(self, other):
        if a != b:
            return False
    return True

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

格式化的内容就忽略啦,主要就是 __format__ 方法的实现。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值