流畅的python_流畅的python:序列的修改、散列和切片

有同学反馈说文章不能留言评论,主要是我的公众号注册很晚,还没有这个功能,另外说实话我写的这玩意儿,我的这个语言水平,也没啥评论的434dc02ad721bf90c1d6e2c3e6ff428d.png。不过为了给大家吐槽的地方,借助第三方小程序可以评论吐槽,点击最下面的小程序卡片即可。欢迎吐槽!!!

催更或者公众号意见可以直接在公众号留言即可,我会看的哦。

第10章 序列的修改、散列和切片

  • 1、初始化-向下兼容

  • 2、Vector表示形式

  • 3、协议和鸭子类型

  • 4、动态存取属性

  • 5、可散列的实现

    • 5.1 __hash__的实现

    • 5.2 __eq__的改进

  • 6、小结

前面我们讲到二维向量Vector,接下来我们扩展到高维向量,引出序列的高级操作。

1、初始化-向下兼容

兼容性是我们在开发程序时必须要考虑的问题。前面我们通过Vector(3,4)来实现初始化,但是序列类型的构造方法最好接受可迭代的对象为参数,也就说,通过Vector([3,4])类似的形式进行初始化是大多数序列类型数据初始化的标准,所以我们采用*ags收集参数,如果长度为1,则表明是可迭代参数对象,获得的结果是([3, 4],),只需取处arg中的第一个元素即可;如果长度不为1,则是类似于上一章的初始化方式,获得的arg应该是(3, 4):

from array import array


class Vector():
    typecode = 'd'

    def __init__(self, *args):
        compents = args[0] if len(args) == 1 else args
        self.__compents = array(self.typecode, compents)

同样,我们将上一个例子中的常规方法引用到新的Vector中:

from array import array


class Vector():
    typecode = 'd'

    def __init__(self, *args):
        compents = args[0] if len(args) == 1 else args
        self.__compents = array(self.typecode, compents)

    def __iter__(self):
        return iter(self.__compents)

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

    def __abs__(self):
        return sum(i*i for i in self)**0.5

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


# 剩下的自己补充吧

为了回顾以前的知识,请大家思考这样一个问题:为什么__iter__实现的时候使用的是self.__compents,__abs__使用的是self,而不是self.__compents?这个其实很好解释,因为self中包含了其他很多的方法和属性,例如typecode, __dict__等等。而我们真正需要的是self.__compents,所以使用self.__compents构造迭代对象。__abs__使用的是列表表达式来计算,for i in x:这个语句,背后其实用的是iter(x),所以看着是使用的self,但是本质上利用的是iter(self),而迭代特性iter(self)就是只由self.__compents通过__iter__来实现,所以使用self即可,当然使用self.__compents也没有什么错,因为这里的self.__compents是一个array对象,本身就是一个可迭代对象。

2、Vector表示形式

前面我们已经学到的实现方法有__str__, __repr__, __format__, __bytes__, frombytes, 分别对应着str, repr, format, bytes, frombytes,还记得它们的区别吗?str是面向用户的,而repr是面向程序员的,repr的表示形式更加友好。format实现Vector按照我们所需形式进行格式化。bytes和frombytes实现从Vector对象和字节序列的相互转换。接下来我们来实现它们:

from array import array
import reprlib
import math


class Vector():
    typecode = 'd'

    def __init__(self, *args):
        compents = args[0] if len(args) == 1 else args
        self.__compents = array(self.typecode, compents)

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

    def __iter__(self):
        return iter(self.__compents)

    def __abs__(self):
        return sum(i*i for i in self)**0.5

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

    def __str__(self):
        return str(tuple(self.__compents))

    def __repr__(self):
        class_name = type(self).__name__
        #         return "{}{}".format(class_name, self)
        vr = reprlib.repr(self.__compents)
        vr_real = vr[vr.find('[')+1:-2]
        return '{}({})'.format(class_name, vr_real)

    def __format__(self, fmt_str: str):
        if fmt_str.endswith('h'):
            r = abs(self)
            phi = [math.acos(i/r)/math.pi*180 for i in self]
            out_str = [format(i, fmt_str[:-1]) for i in [r]+phi]
            return ''.format(', '.join(out_str))
        else:
            out_str = [format(i, fmt_str) for i in self]
            return '({})'.format(', '.join(out_str))

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

    @classmethod
    def frombytes(cls, bytes_list):
        typecode = chr(bytes_list[0])
        mem = memoryview(bytes_list[1:]).cast(typecode)
        return cls(mem)

这里我们对repr的实现进行了改变,如果向量长度很长,使用原来的处理方法会全部列出,所以使用reprlib.repr()函数获取self.__components的有限长度表示形式:
Vector(0.0, 1.0, 2.0, 3.0, 4.0, ...)

上一章在实现__format__方法时如果格式化语言以p结尾则输出极坐标的形式,而对于多维度向量采用超球面坐标进行表示,其基本格式为:,我们指定如果格式化语言以h结尾,则将其表示为超球面坐标形式。

注意:自定义格式化语言代码时不要使用内置格式代码,比如浮点数的格式代码'eEfFgGn%',整数使用的格式代码有'bcdoxXn',字符串使用的's'。所以上一章我们使用p表示极坐标,本章使用h表示超球面坐标。

其余的内容与上一章基本类似,不再赘述了。

3、协议和鸭子类型

在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。鸭子类型指的是:一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。是不是看的一头雾水,举个例子来说,Python的序列协议只需要__len__和__getitem__两个方法,所以只要一个对象实现了这两个方法,那么就可以认为是一个序列。

from array import array
import reprlib


class Vector():
    typecode = 'd'

    def __init__(self, *args):
        compents = args[0] if len(args) == 1 else args
        self.__compents = array(self.typecode, compents)

    # 篇幅限制,省略了大部分方法

    def __len__(self):
        return len(self.__compents)

    def __getitem__(self, index):
        return self.__compents[index]

我们对Vector增加了__len__和__getitem__方法,那么它就可以实现下面的长度、索引、切片功能:

v=Vector(range(10))
print(len(v))
print(v[2])
print(v[2:5])

# 返回
10
2.0
array('d', [2.0, 3.0, 4.0])

因为它和序列有着相似的行为模式,所以可以认为就是序列。不过仍然存在不完美的地方,因为切片得到的应该是Vector的新实例,而不应该是array。所以有必要对index进行一个判断,如果index就是一个整数的话,应该返回index对应的值,如果是切片的话,应该返回一个新的Vector对象。

  • slice对象

我们先深入了解一下切片和索引所返回index的区别:

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


s = Myseq()
print(s[1])
print(s[2:4])
print(s[0:6:2, 9])
print(s[0:6:2, 1::2])

# 输出
1
slice(2, 4, None)
(slice(0, 6, 2), 9)
(slice(0, 6, 2), slice(1, None, 2))

可以看到对于索引来说,返回的为单个数值;对于切片来说,返回的是slice对象;而存在多个的时候,将会以元组的形式组合起来。

slice中有一个indices方法可以将不同形式的切片S规范化,默认情况下的切片通过:S.indices(len)转换为标准的切片形式(start、stop和stride不存在负值或者None)。如果stop超过了len,则会根据len进行截断。

slice(2, -1, 2).indices(5) -> (2, 4, 2)
slice(3,None,None).indices(7) -> (3, 7, 1)
s[0:6:2].indices(4) -> (0, 4, 2)
  • 切片的优化
from array import array
import reprlib


class Vector():
    typecode = 'd'

    def __init__(self, *args):
        compents = args[0] if len(args) == 1 else args
        self.__compents = array(self.typecode, compents)

    # 篇幅限制,省略了大部分方法

    def __len__(self):
        return len(self.__compents)

    def __getitem__(self, index):
        cls = type(self)
        if isinstance(index, slice):
            return cls(self.__compents[index])
        elif isinstance(index, int):
            return self.__compents[index]
        else:
            raise TypeError('index must be integers')

4、动态存取属性

自从我们将Vector转换为利用可迭代对象进行初始化以后,就不能像上一章一样使用v.x,v.y的格式进行属性的获取。在高维向量中,使用x,y,z,t来获取前几个数据有可能为我们提供方便,接下来我们来实现它。

  • @property

当然我们可以像上一章一样使用@property装饰器把x和y标记为只读特性:

@property
def x(self):
    return self.__compents[0]
# 类似的,分别设置y,z,t

尽管可以实现属性的获取,但是这样太麻烦了,特殊方法__getattr__或许是一个更好的选择。

  • __getattr__

__getattr__可以实现对类属性的获取,例如v.x的形式。前面我们介绍过四个与属性有关的魔法方法,__setattr__、__getattr__、__getattribute__与__delattr__,如果忘记的话快回去看看吧。

需要注意的是:前面讲的__getattr__,支持像v[0]这样基于索引的获取;__getattr__是像v.x这样对类属性的获取,注意两者的区别。

首先我们来看一个简单的实现:

    def __getattr__(self, name):
        if len(name) == 1:
            pos = 'xyzt'.find(name)
            if pos >= 0:
                return self.__compents[pos]

        raise AttributeError('Vector: Not Found '+name)

是不是感觉这样实现很简单,但是却存在着问题:

>>> v = Vector(2, 3, 4, 5)
>>> v.x
2.0
>>> v.r
AttributeError: Vector: Not Found r
>>> v.x = 10
>>> v.x
10
>>> v
Vector(2.0, 3.0, 4.0, 5.0)

首先,v.x赋值的时候应该抛出异常,但是并没有。另外,就算赋值以后前后逻辑也不对,我们明明将第一个数赋值为10,但是实际向量并没有发生变化,怪异的是,我们调用v.x确实又返回了10。这个主要是由__getattr__的工作方式影响的:

简单来说(实际过程比这个更加复杂,后面章节我们会讲到),对my_obj.x表达式,Python会检查my_obj实例有没有名为x的属性;如果没有,到类(my_obj.__class__)中查找;如果还没有,顺着继承树继续查找。如果依旧找不到,才会调用my_obj所属类中定义的__getattr__方法,传入self和属性名称的字符串形式(如'x')。

所以,当我们执行完像v.x=10这样的赋值语句之后,v对象有了x属性,后面访问会率先访问该属性,根本就到达不了__getattr__这一步骤。为了避免设置小写字母的实例属性,我们增加了__setattr__方法:

def __setattr__(self, name: str, value):
        cls = type(self)
        if len(name) == 1:
            # 禁止设置xyzt为属性名的值
            if name in 'xyzt':
                msg = 'readonly attribute {!r}'.format(name)
                raise AttributeError(msg)
                #  !r 先应用 repr(attr_name) 再进行格式化,
                #  类似的还有!a(ascii()),!s(str())
            # 禁止设置小写26字母为属性名
            elif name.islower():
                msg = "cannot set attribute 'a' to 'z' in {cls_name!r}".format(
                    cls_name=cls.__name__)
                raise AttributeError(msg)
        super().__setattr__(name, value)

这个例子也给我们了一个很大的提示:为了保证对象的一致性,如果实现了__getattr__方法,那么多数情况下也要定义__setattr__方法。

5、可散列的实现

前面我们已经讲过,要想将Vector实例变成可散列的,必须实现__hash__、以及__eq__方法,而且要保证向量不可变。

5.1 __hash__的实现

上一章我们讲过,对于多个分量求哈希运算,常用做法有:

  • 将所有分量转换为一个元组,返回元组的哈希
  • 各个分量的哈希值之间进行异或运算,返回最终结果 所以可以简单的将array转换为tuple,计算哈希值:
def __hash__(self):
    return hash(tuple(self.__compents))

也可以通过异或的方式求得。对于多元素求最后的归约值,可以有以下三种方式:

# 方式1:利用for循环
import operator

n = 0
for i in range(x):
    n ^= i
    
# 方式2:reduce和lambda
import functools
functools.reduce(lambda a, b: a ^ b, range(6))

# 方式3:reduce和operator
import functools
import operator
functools.reduce(operator.xor, range(6))

选择哪一种全靠自己的编程习惯或者规定要求,相比之下我更喜欢第三种方式,解释性更加好一点。还记得reduce函数吗?我们以前曾提过他,归约函数(reduce、sum、any、all)把序列或有限的可迭代对象变成一个聚合结果,很好符合我们对诸多分量求异或的要求。使用reduce函数时最好提供第三个参数,reduce(function, iterable,initializer),这样当iterable为空的时候,会返回initializer这个结果,否则在归约中使用它作为第一个参数,因此应该使用恒等值。比如,对+、|和^来说, initializer应该是0;而对*和&来说,应该是1。

def __hash__(self):
    hashs = (hash(x) for x in self.__compents)
    return functools.reduce(operator.xor, hashs, 0)

5.2 __eq__的改进

是时候改进下我们前面写的勉强能用的__eq__了。前面利用简单的利用tuple(self) == tuple(other)进行相等的判断,尽管很简洁,但是效率低下,这样一个一个比对,对于多维向量来说实在是一个负担。当然,还存在着Vector([1, 2])和(1, 2)相等的问题,我们在后面会再次进行改进。

代码思路:首先对两个序列的长度进行判断,如果长度不一样,则返回False。然后zip函数生成一个由元组构成的生成器,一旦存在不一致的元素,则立即返回False:

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 __eq__(self, other):
    return len(self) == len(other) and all((a == b for a, b in zip(self, other)))

内置的zip和enumerate函数是十分pythonic的语法,所以一定要掌握他们。另外,稍微说点题外话,zip函数的名字取自拉链系结物(zipper fastener),因为这个物品用于把两个拉链边的链牙咬合在一起,这形象地说明了zip(left, right)的作用。zip函数与文件压缩没有关系。

6、小结

本章继续对Vector进行了改进,对初始化方式以及Vector表示形式进行了进一步的说明。然后对更多的魔法方法进行了说明:有可以实现索引取值以及切片的__getitem__方法;有实现类属性获取的__getattr__方法;有实现解包的__iter__的方法;有实现可散列的__hash__,__eq__方法。。。与上一章一样,我们分析Python标准对象的行为,然后进行模仿,让Vector的行为符合Python风格。

——本章完—— 

欢迎关注我的微信公众号b8a91dad03b0161f996bd3ac3b460629.png

如果能点个在看就好了6c2bc2da289a30defc765720f81d214c.png

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值