Python编程:序列的修改、散列和切片

1.Vector类:用户定义的类型

        我们将使用组合模式实现Vector类,而不使用继承。向量的分量在浮点数数组中,而且将实现不可变扁平序列的实现方法。

        不过,在实现序列方法之前,我们要确保Vector类与之前定义的Vector2d类兼容。除非有些地方让二者兼容没有什么意义。

        下面这段代码,就是我们这一章要实现的Vector类的代码。现在先给大家提供出来,后续我们将一步步实现。

import numbers
import reprlib
from array import array
import functools
import operator
import itertools
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 (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)
        return functools.reduce(operator.xor, hashes, 0)

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

    def __bool__(self):
        return bool(abs(self))

    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 = '{.__name__}indices must be integers'
            raise TypeError(msg.format(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 ''a'' to ''z'' in {cls_name!r}'
            elif name.islower():
                error = 'can not set attribute ''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)
    
    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)], self.angles())
            outer_fmt = '<{}>'
        else:
            coords = self
            outer_fmt = '({})'
        compontents = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(','.join(compontents))
    
    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)

2.Vector类第1版:与Vector2d类兼容

        为了让Vector接受多维向量,我们可以让_init_方法接受任意个参数(通过*args),但是序列类型的对象最好接受可迭代类型的参数,因为所有的内置类型都是这样做的。     

from vector import Vector

print(Vector([2.1, 3.1]))
print(Vector(range(10)))

        通过上面代码的测试,我们可以确保当传入两个及以上分量时,测试都能通过。而且得到的结果都相同。不过,我们会发现,当Vector实例的分量超过6个时,repr()生成的字符串会使用...省略一部分数据。包含大量元素的集合类型一定是要这么做的,因为字符串的表示形式是用来调试的。

import numbers
import reprlib
from array import array
import functools
import operator
import itertools
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 (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)

        上面的代码是我们第一版Vector类的实现代码。我们首先是将self._components通过前下划线变成一个受保护的实例,并将其保存在一个数组当中。然后,为了迭代,我们使用iter构造一个迭代器。并且我们使用reprlib.repr()函数来获取self._components有效长度的表示形式,在字符串插入Vector的调用方法之前,去掉前面的array的‘d’。计算绝对值时,不能再使用hypot方法了,因此,我们先计算各分量的平方和,再利用sqrt进行开方。

3.协议和鸭子类型

       前面我们讲过,在Python中创建功能完善的序列类型无需使用继承,只需要实现符合序列协议的方法即可。那么,在面向对象的编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。例如,Python序列协议只需要_len_和_getitem_这两个方法。任何类,只要使用标准的签名和语义实现了这两种方法,就能用在任何期待序列的地方。如:Spam是不是谁的子类无关紧要,只要提供了所需的方法即可。

        另外,协议是非正式的,没有强制力,因此如果你知道类的具体使用场景,通常只需要实现协议的一部分即可。

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

        首先,我们先了解一下切片的原理。先看一下下面的例子:

class MySeq:
    def __getitem__(self, index):
        return index

s = MySeq()
print(s[1])
print(s[1:4:2])
print(s[1:4:2, 9])

        我们定义了一个简单的类,里面只有一个方法,即返回传给它的值。我们首先我们打印了单个索引值,这没有什么新奇的。重点在后面,我们打印s[1:4:2](1指起始值,4指结束值,2是步长),会发现返回的是切片,如果[]其中有逗号,那么返回的是一个元组。

        我们通过查看slice会发现,其中有个内置属性indices,它的功能是给定长度为len的序列,计算S表示的扩展切片的起始和结尾索引以及步幅。超出边界的索引会被截掉,这与常规的片片方式一样。

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

4.1能处理切片的_getitem_方法

        下面的示例中给出了让Vector表现为序列的方法:_len_和_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 = '{.__name__}indices must be integers'
            raise TypeError(msg.format(cls))

       在_getitem_方法中,首先,我们获取self类的类型,以供后面使用。然后,如果index参数是slice对象的话,就执行if语句。如果,index参数是int或其他类型的整数,就执行elif语句,否则的话,就抛出异常。

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

        在Vector中,因为是多维向量,我们无法使用名称来访问向量的分量了。现在我们处理的向量可能有大量的分量。不过,若能通过单个字母访问前几个向量的话,会比较方便。比如,用x、y、z代替v[0],v[1],v[2]。

        下面,我们通过定义_getattr_方法来实现这一目的。

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

        首先,获取Vector类,后面待用。然后检查属性名是否只有一个字母,如果是,检查其是不是shortcut_names中的一个。如果位置落在范围内,就返回数组中对应的元素。如果测试失败,就抛出异常,并指明标准的消息文本。

        但是,这样的实现方法还不够,因为你会发现先给v.x赋值会覆盖原有的值。这是_getattr_运作方式导致的:仅当对象没有指定的属性时,Python才会调用那个方法,这是一种后备机制。此外,_getattr_方法也没有考虑到self._compontents之外的实例属性,而是从这个属性中获取shortcut_names所列的虚拟属性。

        为了避免这个矛盾,我们要改写设置属性的逻辑。即:在Vector类中,如果为名称是单个小写字母的属性赋值,我们抛出异常。为此,我们要实现_setattr_方法。

    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:
            if name in cls.shortcut_names:
                error = 'readonly attribute ''a'' to ''z'' in {cls_name!r}'
            elif name.islower():
                error = 'can not set attribute ''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)

        上面的代码中,特别处理名称是单个字符的属性。如果name是xyzt中的一个,设置特殊错误消息。如果name是小写字母,为所有的小写字母设置一个错误消息。否则的话,把错误消息设置为空字符串。如果有错误消息,就抛出异常。默认情况下,在超类上调用_setattr_方法,提供标准行为。

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

        我们要实现_hash_方法,加上现有的_eq_方法,这会把Vector实例变成可散列的对象。在这一章中,我们将使用异或(^)运算符依次计算各个分量的散列值。

    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)
        return functools.reduce(operator.xor, hashes, 0)

        上面的代码中,_eq_方法中首先判断两个对象的长度,如果长度不一样,二者就不相等,zip函数生成一个元组,元组中的元素来自传入参数的各个可迭代对象,只要有两个分量不同,就返回False。_hash_方法中,我们创建了一个生成器表达式,惰性计算各个分量的散列值。再把hashes提供给reduce函数,使用xor计算聚合散列值,这其中第三个参数是初始值。reduce函数使用的一般形式如下:

reduce(function, iterable, initializer)

7.Vector类第5版:格式化

        Vector类的_format_方法与Vector2d的类似。但是,不使用极坐标,使用的是球面坐标,因为Vector类支持n维向量,而超过四维后,球体就变成了超球体,因此,我们也会把自定义的格式后缀改成‘h’。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值