Python学习之路29-序列的修改、散列和切片

《流畅的Python》笔记。

本篇是“面向对象惯用方法”的第三篇。本篇将以上一篇中的Vector2d为基础,定义多维向量Vector。python

1. 前言

自定义Vector类的行为将与Python标准中的不可变扁平序列同样,它将支持以下功能:编程

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

本篇还将经过__getattr__方法实现属性的动态存取(虽然序列类型一般不会这么作),以及穿插讨论一个概念:把协议当作正式接口。咱们将说明协议和鸭子类型之间的关系,以及对自定义类型的影响。bash

2. 第一版Vector

Vector的构造方法将和全部内置序列类型同样,以可迭代对象为参数。若是其中元素过多,repr()函数返回的字符串将会使用...省略一部份内容,它的初始版本以下:微信

# 代码1
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):   # 和Vector2d相同
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) + bytes(self._components))

    def __eq__(self, other):   # 和Vector2d相同
        return tuple(self) == tuple(other)

    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))

    def __bool__(self):   # 和Vector2d相同
        return bool(abs(self))

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)   # 去掉了Vector2d中的星号*

之因此没有直接继承制Vector2d,既是由于这两个类的构造方法不兼容,也是由于咱们要为Vector实现序列协议。ssh

3. 协议和鸭子类型

协议和鸭子类型在以前的文章中也有所说起。在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。函数

在Python中,只要实现了协议须要的某些方法,其实就算实现了协议,而不必定须要继承。好比只要实现了__len____getitem__这两个方法,那么这个类就是知足序列协议的,而不须要从什么“序列基类”继承。测试

鸭子类型:和现实中相反,Python中肯定一个东西是否是“鸭子”,不是测它的“DNA”是否是”鸭子“的DNA,而是看这东西像不像只鸭子。只要像”鸭子“,那它就是“鸭子”。好比,只要一个类实现了__len____getitem__方法,那它就是序列类,而没必要管它是从哪来的;文件类对象也常是鸭子类型。网站

4. 第2版Vector:支持切片

Vector变为序列类型,并能正确返回切片:spa

# 代码2,将如下代码添加到第一版Vector中
class Vector:
    -- snip --
    def __len__(self):
        return len(self._components)
    
    def __getitem__(self, index):
        cls = type(self)
        if isinstance(index, slice):  # 若是index是个切片类型,则构造新实例
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral):  # 若是index是个数,则直接返回
            return self._components[index]
        else:
            msg = "{cls.__name__} indices must be integers"
            raise TypeError(msg.format(cls=cls))

若是__getitem__函数直接返回切片:return self._components[index],那么获得的数据将是array类型,而不是Vector类型。正是为了使切片的类型正确,这里才作了类型判断。.net

上述代码中用到了slice类型,它是Python的内置类型,这里顺便补充一下切片原理,直接上代码:

# 代码3
>>> class MySeq:
...     def __getitem__(self, index):
...         return index  # 直接返回传给它的值
...    
>>> s = MySeq()
>>> s[1]   
1  # 单索引,没啥新奇的
>>> s[1:3]
slice(1, 3, None)  # 返回来一个slice类型
>>> s[1:10:2]
slice(1, 10, 2)    # 注意slice类型的结构
>>> s[1:10:2, 9]
(slice(1, 10, 2), 9)   # 若是[]中有逗号,__getitem__收到的是元组
>>> s[1:10:2, 7:9]
(slice(1, 10, 2), slice(7, 9, None))

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

当咱们用dir()函数获取slice的属性时,发现它有startstopstep数据属性,而且还有一个indices方法,这里重点说说这个indices方法。它接收一个长度参数len,并根据这个lenslice类型的startstopstep三个参数正确转换成在长度范围内的非负数,具体用法以下:

# 代码4
>>> slice(None, 10, 2).indices(5)
(0, 5, 2)  # 将这些烦人的索引通通转换成明确的正向索引
>>> slice(-3, None, None).indices(5)
(2, 5, 1)

自定义Vector类中并无使用这个方法,由于Vector的底层咱们使用了array.array数据类型,切片的具体操做不用咱们自行编写。但若是你的类没有这样的底层序列类型作支撑,那么slice.indices方法将为你节省大量时间。

5. 第3版Vector:动态存储属性

目前版本的Vector中,没有办法经过名称访问向量的份量(如v.xv.y),并且如今的Vector可能存在大量份量。不过,若是能经过单个字母访问前几个份量的话,这样将很方便,也更人性化。如今,咱们想用xyzt四个字母分别代替v[0]v[1]v[2]v[3],但具体作法并非为实例添加这四个属性,而且咱们也不想在运行时实例能动态添加单个字母的属性,更不想实例能经过这四个字母修改Vectorself._components的值。换句话说,咱们只想经过这四个字母提供一种较为方便的访问方式,仅此而已。而要实现这样的功能,则须要实现__getattr____setattr__方法,如下是它们的代码:

# 代码5.1
class Vector:
    -- snip --
    
    shortcut_name = "xyzt"

    def __getattr__(self, name):
        cls = type(self)
        if len(name) == 1:   # 若是属性是单个字母
            pos = cls.shortcut_name.find(name)    
            if 0 <= pos < len(self._components):  # 判断是否是xyzt中的一个
                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:   # 不容许建立单字母实例属性,即使是x,y,z,t
            if name in cls.shortcut_name:  # 若是name是xyzt中的一个,设置特殊的错误信息
                error = "readonly attibute {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__方法。简单来讲,对my_obj.x表达式,Python会检查my_obj实例有没有名为x实例属性;若是没有,则到它所属的类中查找有没有名为x类属性;若是仍是没有,则顺着继承树继续查找。若是依然找不到,则会调用my_obj所属类中定义的__getattr__方法,传入self和属性名的字符串形式(如'x');
  • __getattr____setattr_方法通常同时定义,不然对象的行为很容易出现不一致。好比,若是这里定义__getattr__方法,则会出现以下尴尬的代码:

    # 代码5.2
    >>> 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
    10
    >>> v  # 实际上是v建立了新实例属性x,这也是为何咱们要定义__setattr__
    Vector([0.0, 1.0, 2.0, 3.0, 4.0])   # 行为不一致
  • 咱们没有禁止动态添加属性,只是禁止为单个字母属性赋值,若是属性名的长度大于1,这样的属性是能够动态添加的;
  • 若是你看过上一篇文章,那么你可能会想到用__slots__来禁止添加属性,但咱们这里仍然选择实现__setattr__来实现此功能。__slots__属性最好只用于节省内存,并且仅在内存严重不足时才用它,别为了秀操做而写一些别人看着很别扭的代码(只写给本身看的除外)。

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

目前这个Vector是不可散列的,如今咱们来实现__hash__方法。具体方法和上一篇同样,也是用各个份量的哈希值进行异或运算,因为Vector的份量可能不少,这里咱们使用functools.reduce函数来归约异或值。同时,咱们还将改写以前那个简洁版的__eq__,使其更高效(至少对大型向量来讲更高效):

# 代码6,请自行导入所需的模块
class Vector:
    -- snip --
    def __hash__(self):
        hashs = (hash(x) for x in self._components)   # 先求各个份量的哈希值
        return functools.reduce(operator.xor, hashs, 0)  # 而后将全部哈希值归约成一个值

    def __eq__(self, other): # 不用像以前那样:生成元组只为使用元组的__eq__方法
        return len(self) == len(self) and all(a == b for a, b in zip(self, other))

解释:

  • 此处的__hash__方法实际上执行的是一个映射归约的过程。每一个份量被映射成了它们的哈希值,这些哈希值再归约成一个值;
  • 这里的functool.reduce传入了第三个参数,而且建议最好传入第三个参数。传入第三个参数能避免这个异常:TypeError: reduce() of empty sequence with no initial value。若是序列为空,第三个参数就是返回值;不然,在归约中它将做为第一个参数;
  • __eq__方法中先比较两序列的长度并不只仅是一种捷径。zip函数并行遍历多个可迭代对象,若是其中一个耗尽,它会当即中止生成值,并且不发出警告;
补充一个小知识: zip函数和文件压缩没有关系,它的名字取自拉链头(zipper fastener),这个小物件把两个拉链条的链牙要合在一块儿,是否是很形象?

7. 第5版Vector:格式化

Vector2d中,当传入'p'时,以极坐标的形式格式化数据;因为Vector的维度可能大于2,如今,当传入参数'h'时,咱们使用球面坐标格式化数据,即'<r, Φ1, Φ2, Φ3>'。同时,还须要定义两个辅助方法:

  • angle(n),用于计算某个角坐标;
  • angles(),返回由全部角坐标构成的可迭代对象。

至于这两个的数学原理就不解释了。如下是最后要添加的代码:

# 代码7
class Vector:
    -- snip --
    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
        return a

    def angles(self):
        return (self.angle(n) for n in range(1, len(self)))

    def __format__(self, format_spec=""):
        if format_spec.endswith("h"):   # 若是格式说明符以'h'结尾
            format_spec = format_spec[:-1]   # 格式说明符前面部分保持不变
            coords = itertools.chain([abs(self)], self.angles())  # 
            outer_fmt = "<{}>"
        else:
            coords = self
            outer_fmt = "({})"
        components = (format(c, format_spec) for c in coords)
        return outer_fmt.format(", ".join(components))

itertools.chain函数生成生成器表达式,将多个可迭代对象链接成在一块儿进行迭代。关于生成器的更多内容将在之后的文章中介绍。

至此,多维Vector暂时告一段落。


迎你们关注个人微信公众号"代码港" & 我的网站 www.vpointer.net ~

相关文章
相关标签/搜索