资讯专栏INFORMATION COLUMN

Python中的序列修改、散列和切片

jubincn / 2003人阅读

摘要:一基本的序列协议首先,需要就维向量和二维向量的显示模的计算等差异重新调整。假设维向量最多能处理维向量,访问向量分量的代码实现如下若传入的参数在备选分量中可进行后续处理判断分量的位置索引是否超出实例的边界不支持非法的分量访问,抛出。

导语:本文章记录了本人在学习Python基础之面向对象篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。

本文重点:

1、了解协议的概念以及利用__getitem__和__len__实现序列协议的方法;
2、掌握切片背后的__getitem__;
3、掌握动态访问属性背后的__getattr__和__setattr__;
4、掌握实现可散列对象背后精简的__hash__和__eq__。

注:本文介绍的vector类将二维vector类推广到多维,跟不上本文的朋友可以移步至《编写符合Python风格的对象》先了解二维向量类的编写。

一、基本的序列协议

首先,需要就n维向量和二维向量的显示、模的计算等差异重新调整。n维向量的设计包括初始化,迭代,输出,向量实例转为字节序列,求模,求布尔值,比较等内容,代码如下:

import math
import reprlib
from array import array

class Vector:
    typecode="d"
    def __init__(self,components):
        self._components=array(self.typecode,components)

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

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

    def __repr__(self):
        classname=type(self).__name__
        components=reprlib.repr(self._components)
        components=components[components.find("["):-1]
        return "{}({})".format(classname,components)

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

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

    def __bytes__(self):
        return (bytes(self.typecode,encoding="utf-8")+
                bytes(array(self.typecode,self._components)))

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

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

在Python中创建功能完善的序列类型无需使用继承,只需要实现符合序列协议的__len__和__getitem__,具体代码实现如下:

class Vector:
    #省略中间代码
    def __len__(self):
        return len(self._components)
    
    def __getitem__(self, item):
        return self._components[item]

在面向对象编程中,协议是非正式的接口,没有强制力。因此如果知道类的具体使用场景,实现协议中的一部分也可以。例如,为了支持迭代只实现__getitem__方法即可。

二、切片原理 1、了解切片的行为

在对序列切片(slice)的操作中,解释器允许切片省略start,stop,stride中的部分值甚至是全部省略。通过dir(slice)查阅发现,是切片背后的indices在做这个工作。indices方法会整顿存储数据属性的元组,把start,stop,stride都变成非负数,而且都落在指定长度序列的边界内。
例如slice(-3,None,None).indices(5)整顿完毕之后是(2,5,1)这样合理的切片。

2、关键的__getitem__方法

__getitem__是支持迭代取值的特殊方法。我们将上文的__getitem__改造成可以处理切片的方法,改造需要考虑到处理参数是否为合理切片,合理切片的操作结果是产生新的向量实例。

    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))#判断参数不合理时抛出TypeError
三、动态存取属性 1、访问向量分量:__getattr__

n维向量没有像二维向量一样把访问分量的方式直接在__init__中写入,由于传入的维数不确定无法采取穷举分量的原始方法,为此我们需要借助__getattr__实现。假设n维向量最多能处理6维向量,访问向量分量的代码实现如下:

    shortcut_names="xyztpq"
    def __getattr__(self, name):
        cls=type(self)
        if len(name)==1:
            index=cls.shortcut_names.find(str(name))#若传入的参数在备选分量中可进行后续处理
            if 0<=index

Tips:代码严谨之处在于传入的参数即使在备选分量之中,也有可能会超出实例的边界,因此涉及到索引和边界需要认真注意这一点。

2、保持行为一致:__setattr__

尽管我们实现了__getattr__,但事实上目前的n维向量存在行为不一致的问题,先看一段代码:

v=Vector(range(5))
print(v.y)#输出1.0
v.y=6
print(v.y)#输出6
print(v)#输出(0.0, 1.0, 2.0, 3.0, 4.0)

上面的例子显示我们可以访问6维向量的y分量,但是问题在于我们为y分量赋值的改动没有影响到向量实例v。这种行为是不一致的,并且还没有抛出错误令人匪夷所思。本文中我们希望向量分量是只读不可变的,也就是说我们要对修改向量分量这种不当的行为抛出Error。因此需要额外构造__setattr__,代码实现如下:

    def __setattr__(self, key, value):
        cls=type(self)
        if len(key)==1:
            if key in self.shortcut_names:
                error="can"t set value to attribute {attr_name!r}"
            elif key.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=key)
                raise AttributeError(msg)
        super().__setattr__(key,value)#在超类上调用__setattr__方法来提供标准行为。

小结:如果定义了__getattr__方法,那么也要定义__setattr__方法,这样才能避免行为不一致。

四、可散列的对象

可散列对象应满足的三个条件在此不再赘述,对于n维向量类而言需要做两件事将其散列化:

1、利用异或运算符构造__hash__

构造思路是将hash()应用到向量中的每个元素,并用异或运算符进行聚合计算。由于处理的向量维数提高,采用归约函数functools.reduce处理。

import operator
from functools import reduce
    def __hash__(self):
        hashes=map(hash,self._components)
        return reduce(operator.xor,hashes)
2、通过zip优化n维向量的比较方法__eq__

上文初始给出的比较方法是粗糙的,下面针对两个维数均不确定的向量进行比较,代码如下:

 def __eq__(self, other):
     if len(self)!=len(other):#数组数量的比较很关键
         return False
     for x,y in zip(self,other):
         if x!=y:
             return False
     return True

数组数量的比较时很关键的,因为zip在比较数量不等的序列时会随着一个输入的耗尽而停止迭代,并且不抛出Error。
回到正题,上述的逻辑关系可以进一步精简。通过all函数可以把for循环替代:

    def __eq__(self, other):
         return len(self)==len(other) and all(x==y for x,y in zip(self,other))

本人更喜欢后者这种简洁且准确的代码书写方式。

五、格式化显示

理解n维向量的超球面坐标(r,θ1,θ2,θ3,...,θn-1)计算公式需要额外的数学基础,此处的格式化输出在本质上与《编写符合Python风格的对象》中的格式化输出并无明显区别,此处不作详述,感兴趣的朋友可以查看如下的代码:

    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()) #利用itertools.chain无缝迭代模和角坐标
            outer_fmt = "<{}>" 
        else:
            coords = self
            outer_fmt = "({})" 
        components = (format(c, fmt_spec) for c in coords) #格式化极坐标的各元素并存入生成器中
        return outer_fmt.format(", ".join(components)) 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/41277.html

相关文章

  • Python学习之路29-序列修改列和切片

    摘要:具体方法和上一篇一样,也是用各个分量的哈希值进行异或运算,由于的分量可能很多,这里我们使用函数来归约异或值。每个分量被映射成了它们的哈希值,这些哈希值再归约成一个值这里的传入了第三个参数,并且建议最好传入第三个参数。 《流畅的Python》笔记。本篇是面向对象惯用方法的第三篇。本篇将以上一篇中的Vector2d为基础,定义多维向量Vector。 1. 前言 自定义Vector类的行为...

    马忠志 评论0 收藏0
  • 流畅的python读书笔记-第十章-序列修改列和切片

    摘要:例如,的序列协议只需要和两个方法。任何类如,只要使用标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。 序列的修改、散列和切片 接着造Vector2d类 要达到的要求 为了编写Vector(3, 4) 和 Vector(3, 4, 5) 这样的代码,我们可以让 init 法接受任...

    cpupro 评论0 收藏0
  • Python入门学习笔记汇总

    摘要:导语本文章汇总了本人在学习基础之绪论篇数据结构篇函数篇面向对象篇控制流程篇和元编程篇学习笔记的链接,打算入门的朋友们可以按需查看并交流。 导语:本文章汇总了本人在学习Python基础之绪论篇、数据结构篇、函数篇、面向对象篇、控制流程篇和元编程篇学习笔记的链接,打算入门Python的朋友们可以按需查看并交流。 第一部分:绪论篇 1、Python数据模型 第二部分:数据结构篇 2、序列构成...

    U2FsdGVkX1x 评论0 收藏0
  • 流畅的python

    摘要:流畅的中有很多奇技淫巧,整本书都在强调如何最大限度地利用标准库。常见的扁平序列包括,,等。数组支持所有跟可变序列有关的操作,包括和。和用于指定列表的区间,默认是使用整个列表。但是元组的赋值不被允许,当异发生时 流畅的python中有很多奇技淫巧,整本书都在强调如何最大限度地利用Python 标准库。介绍了很多python的不常用的数据类型、操作、库等,对于入门python后想要提升对p...

    Alan 评论0 收藏0

发表评论

0条评论

jubincn

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<