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

導語:本文章記錄了本人在學習Python基礎之面向對象篇的重點知識及我的心得,打算入門Python的朋友們能夠來一塊兒學習並交流。

本文重點:python

一、瞭解協議的概念以及利用__getitem__和__len__實現序列協議的方法;
二、掌握切片背後的__getitem__;
三、掌握動態訪問屬性背後的__getattr__和__setattr__;
四、掌握實現可散列對象背後精簡的__hash__和__eq__。

注:本文介紹的vector類將二維vector類推廣到多維,跟不上本文的朋友能夠移步至《編寫符合Python風格的對象》先了解二維向量類的編寫。編程

1、基本的序列協議

首先,須要就n維向量和二維向量的顯示、模的計算等差別從新調整。n維向量的設計包括初始化,迭代,輸出,向量實例轉爲字節序列,求模,求布爾值,比較等內容,代碼以下:segmentfault

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__方法便可。ide

2、切片原理

一、瞭解切片的行爲

在對序列切片(slice)的操做中,解釋器容許切片省略start,stop,stride中的部分值甚至是所有省略。經過dir(slice)查閱發現,是切片背後的indices在作這個工做。indices方法會整頓存儲數據屬性的元組,把start,stop,stride都變成非負數,並且都落在指定長度序列的邊界內。
例如slice(-3,None,None).indices(5)整頓完畢以後是(2,5,1)這樣合理的切片。函數

二、關鍵的__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

3、動態存取屬性

一、訪問向量份量:__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<len(self._components):#判斷份量的位置索引是否超出實例的邊界
                return self._components[index]
        else:
            msg = "{.__name__} doesn't have attribute {!r}"
            raise AttributeError(msg.format(cls,name))#不支持非法的份量訪問,拋出Error。

Tips:代碼嚴謹之處在於傳入的參數即便在備選份量之中,也有可能會超出實例的邊界,所以涉及到索引和邊界須要認真注意這一點。設計

二、保持行爲一致:__setattr__

儘管咱們實現了__getattr__,但事實上目前的n維向量存在行爲不一致的問題,先看一段代碼:code

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__方法,這樣才能避免行爲不一致。

4、可散列的對象

可散列對象應知足的三個條件在此再也不贅述,對於n維向量類而言須要作兩件事將其散列化:

一、利用異或運算符構造__hash__

構造思路是將hash()應用到向量中的每一個元素,並用異或運算符進行聚合計算。因爲處理的向量維數提升,採用歸約函數functools.reduce處理。

import operator
from functools import reduce
    def __hash__(self):
        hashes=map(hash,self._components)
        return reduce(operator.xor,hashes)

二、經過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))

本人更喜歡後者這種簡潔且準確的代碼書寫方式。

5、格式化顯示

理解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))
相關文章
相關標籤/搜索