流畅的python笔记(十)序列的修改、散列和切片

目录

前言

一、Vector类:用户定义的序列类型

​二、Vector类第一版:与Vector2d兼容

三、协议和鸭子类型

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

把类序列协议的实现委托给其属性

切片原理

__getitem__和slice

Vector类中能处理切片的__getitem__方法

五、Vector类第三版:动态存取属性

六、Vector第四版:散列和快速等值测试

__hash__

__eq__

zip函数

七、Vector类第五版:格式化


前言

本章定义表示多维向量的Vector类,其中的元素是浮点数,将支持以下功能:

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

此外,还将通过__getattr__方法实现属性的动态存取,虽然序列类型通常不会这么做。

一、Vector类:用户定义的序列类型

二、Vector类第一版:与Vector2d兼容

这里故意不让Vector的构造方法和Vector2d的构造方法兼容,一个用可迭代对象,一个是直接传入各个分量。

        为了编写Vector(3,4)和Vector(3,4,5)这样代码,可以让构造函数__init__接受任意个参数(通过*args),但 是所有内置序列类型的做法都是让构造函数接收可迭代对象为参数。如下实例化方式所示:

如果Vector实例的分量超过6个,repr()生成的字符串就会用 ... 省略一部分,因为对象的字符串表示形式都是用于调试的,不能在控制台一次输出几千行内容,使用reprlib模块可以生成长度有限的表示形式。

        Vector类第一版代码如下:

from array import array
import reprlib
import math

class Vector:
    typecode = 'd'

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

    def __iter__(self):
        return iter(self._components) # 使得Vector实例可迭代,委托给数组实现

    def __repr__(self):
        components = reprlib.repr(self._components) # 使用reprlib.repr函数获取self._components的有限长度表示形式
        components = components[components.find('['):-1] # 上一行得到的components形式是array('d', [ ... ]), 这里里层components.find('[')找到左边的'['的位置,而-1是最后一个位置,即‘(’所在的位置索引,然后对外层的components切片,得到 [ ... ]的表示形式,下一行用于返回Vector的字符串表示形式
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self)) # 先把数组转换成元组形式,再求字符串表示

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) + 
                bytes(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))

    @classmethod
    def frombytes(cls, cotets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode) # 强转成float格式的内存视图
        return cls(memv) # 现在的构造函数直接支持可迭代对象,所以不用像前章时需要*memv拆包


三、协议和鸭子类型

在python中创建功能完善的序列类型无需使用继承,只需实现符合序列协议的方法,协议是指?

        在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。python中的序列协议只需要__len__和__getitem__两个方法,任何类,只要实现了这两个方法,就可以用在任何期待序列的地方,即此类可以归类为序列类型。

        

像这个指派类,因为实现了__len__和__getitem__方法,我们就说它是序列,因为它的行为像序列,因此它就是序列

        这就是所谓的鸭子类型,行为像鸭子,就是鸭子。

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

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

把类序列协议的实现委托给其属性

如果对象中有属性是序列,可以把实现类的__len__和__getitem__方法委托给这个序列属性。如下,我们将Vector类的序列协议实现委托给了属性self._components,而该属性本身是序列类型。

 但上边例子有缺点,即Vector实例切片的结果时一个数组对象,而不是Vector对象。   

切片原理

__getitem__和slice

看如下示例,了解__getitem__和切片的行为:

  1. 自定义__getitem__,直接返回传给它的值,接下来我们会看到当我们用索引或者切片时传给item的究竟是什么。
  2. 当索引切片对象时,__getitem__收到的值就是一个索引值整数。
  3. 当给序列进行切片时,__getitem__收到的值是一个slice对象,即切片对象,这也没有步长。
  4. 当进行带步长的切片时,__getitem__收到的切片对象也是带步长的
  5. 如果切片时有逗号,那么__getitem__收到的是元组,且这里元组的第二个元素是整数
  6. 这里元组的两个元素都是切片对象

slice类的属性如下:

  1. slice是python内置类型。
  2. slice中有start、step、stop等数据属性,以及indices方法。

注意slice的indices方法,其可以返回一个元组(start, stop, stride),用法是:

slice(s, t, st).indices(len) -> (start, stop, stride)

作用是对切片对象进行整顿,即优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。该方法把start,stop和stride都变成非负数,并且都落在指定长度序列的边界内。如下例子所示:

Vector类中能处理切片的__getitem__方法

因为上边实现的__getitem__方法中,切片返回的是数组而不是Vector对象,因此需要改进。

  1.  获取实例所属的类,以供后边构造Vector对象返回。
  2.  判断__getitem__方法收到的是不是slice对象,如果是则是求切片。
  3. 调用Vector类构造函数,使用_components数组切片构造一个新的Vector实例。
  4. 如果__getitem__收到参数是整数
  5. 则返回_components数组中对应索引处的元素
  6. __getitem__收到参数非slice非整数,则抛出异常

五、Vector类第三版:动态存取属性

Vector2d中只有两个分量,因此可以用v.x和v.y来分别访问两个分量,但是多维向量类Vector不能这样干了,如果想要用四个变量x、y、z、t来访问向量对象中的前四个分量,可以用特殊方法__getattr__来实现。具体属性查找过程大致如下:

  1. 对于my_obj.x表达式,先检查my_obj实例中有没有名为x的属性
  2. 如果上一步没找到,则到my_obj.__class__中去查找有没有名为x的类属性,因为类属性可以作为实例属性的默认值
  3. 如果上一步还是没找到,则沿着继承树继续查找......
  4. 如果依旧找不到,才会调用__getattr__方法,__getattr__有两个参数,第一个是self,第二个是'x',即属性名的字符串形式

如下图是一个__getattr__简单实现:

shortcut_names = 'xyzt'

def __getattr__(self, name):
    cls = type(self) # 获取当前类名Vector

    if len(name) == 1: # 属性名只有一个字符,可能是'xyzt'中的某一个
        pos = cls.shortcut_names.find(name) # 查找是否是'xyzt'中的某一个
        if 0 <= pos < len(self._components): # 定位位置
            return self._components[pos]
    msg = '{.__name__!r} object has no attribute {!r}' # 失败则抛出属性错误
    raise AttributeError(msg.format(cls, name))

如上边__getattr__的实现,相当于增加了几个虚拟属性 ‘xyzt’,但是如果只实现__getattr__而没有实现__setattr__的话,可能会导致跟我们设想不同的行为。如下:

  1. 用v.x获取向量第一个元素,由于实现了__getattr__,x相当于是一个虚拟属性,因此访问成功
  2. 给v.x赋新值
  3. 可以看到v.x的值改为10
  4. 发现向量中第一个元素还是0,没有变成10!!!

上边现象出现的原因是,只有当对象查找不到某个属性时,才会调用__getattr__方法。标号1处用v.x访问时,x还是虚拟属性,而这里标号2处给v.x赋值时,会创建一个实例属性x出来,不再是虚拟属性,且给该属性赋值为10,而并没有改变原_components数组的值。为了避免这种行为,我们应该实现__setattr__,即设置改变属性时候的行为。

 为了下一小节讲散列,这里我们需要设置为Vector是只读的。

  1. 特别处理名字是单个字符的属性
  2. 如果name是xyzt中的一个,设置错误消息,提醒这些属性只读
  3. 如果name是一个小写字符,设置对应的错误消息
  4. 否则,错误消息设置为空字符串
  5. 如果有错误消息,则抛出AttributeError
  6. 默认情况下调用超类的__setattr__方法,提供标准行为

虽然在类中声明__slots__属性可以防止设置新实例属性,但是不建议这么做,只有在为了节省内存时才应该使用__slots__。

六、Vector第四版:散列和快速等值测试

__hash__

把各个分量的散列值异或起来,构成整个向量对象的散列值。

这里要用到一个函数,规约函数functools.reduce,其作用是将可迭代对象中前两个元素送入一个二参数函数中计算,然后得到的结果与第三个元素一起再送入二参数函数计算......以此类推。用法为: 

ans = functools.reduce(func, iter, initializer)

其中func是用来计算的二参数函数,比如加法,乘法等,iter是可迭代对象,initializer是当可迭代对象为空时候返回的初始值。

        我们已知operator模块以函数的形式提供了python的全部中缀运算符,因此可以导入operator模块方便代码编写。

        下边是用三种方式计算0~5的累计异或值,第一种使用for循环,后两种使用reduce函数。

可以看到使用operator模块的话就不用写匿名函数了,直接调用xor。

        因此Vector类中的__hash__方法如下:

 标号4处特地创建了一个生成器表达式。

        上例中__hash__函数是一种映射规约计算,即映射过程计算各个分量散列值,规约过程则使用xor运算符聚合所有散列值。

  • 映射(map):把hash函数映射到序列的每一个元素上,得到一个新的序列值。
  • 规约(reduce):用xor运算符聚合所有的元素。

可以把生成器表达式替换成map方法,映射过程更明显:

__eq__

__hash__和__eq__方法需要同时实现。

        上边实现的__eq__方法,需要完成复制两个参数,生成两个元组,然后利用tuple的__eq__方法来判断,如果Vector实例有几千个分量的话,这种做法效率就非常低了。可以使用zip函数来重新实现。如下:

  1. 如果两个对象长度不一样,则它们不相等
  2. zip函数可以生成一个元素是元组的生成器(zip对象),然后拆包给变量a,b
  3. 只要有一个分量不同,就直接返回False
  4. 否则返回True

还可以用all函数来替换标号2处的for循环,all中是一个生成器表达式,只有生成的元素都是True的时候才返回True,否则返回False:

zip函数

zip函数用于并行迭代两个或更多的可迭代对象,返回一个zip对象(生成器),可以拆包。

a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]

z = zip(a, b, c)

print(type(z))
print(z)
print(list(z))

zip有一个奇怪的特性:当一个可迭代对象耗尽后,它不发出警告就停止,解决这个问题可以用itertools.zip_longest函数,其可以使用可选的默认值来填充缺失的值,然后继续产出直到最长的可迭代对象耗尽。

七、Vector类第五版:格式化

略。

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值