第十章 序列的修散列和切片

第十章 序列的修散列和切片

本章將以第9章定義的二維向量類為基礎,向前邁出一大步、定義標識多維向量的Vector類。這個類的行為和Python中標準的不可變扁平序列一樣。該類實力中的元素都是浮點數。最終我們將實現一下功能:

  • 基本的序列協議 --- __len__ 和 __getitem__
  • 正確標書擁有很多元素的實例
  • 適當的切片支持,用於生成新的實例
  • 綜合各個元素的值來計算散列值
  • 自定義的格式語言擴展

於此同時,我們將穿插的討論一個概念: 把協議當做正式的介面。

並說明協議和鴨子類型之間的關以及對自定義類型的影響。

Vector第一版:與Vector2d兼容

我們大可不必重頭來過去寫一個新的類型,

我們將要寫的類只是在我們上一個版本(二維向量)的基礎上

擴展為多維向量,並添加了一些新的特性,

所以我們首先在Vector2d的基礎上開始構建新的類

n多維向量類 v1nnnfrom array import arraynimport reprlibnimport mathnnclass Vector:n typecode = dnn def __init__(self, components):n # 用 _ 代表受保護的屬性,把Vector的分量保存在一個數組之中n self._components = array(self.typecode, components)nn def __iter__(self):n # 通過_components 來構造一個迭代器n return iter(self._components)nn def __repr__(self):n # 使用reprlib.repr()函數來獲取分量的有限長度的表現形式(防止出現1000維度的實例顯示問題)n components = reprlib.repr(self._components)n # 通過字元串生成實例是 刪掉【 之前和array(d)後面的n components = components[components.find([):-1]n return Vector({}).format(components)nn def __str__(self):n return str(tuple(self))nn def __bytes__(self):n # 這裡我們可以直接使用_components 來構建bytes對象n return (bytes([ord(self.typecode)]) + bytes(self._components))nn def __eq__(self, other):n return tuple(self) == tuple(other)nn def __abs__(self):n # 通過計算各個分量的平方和來算出絕對值(距離)n return math.sqrt(sum(x * x for x in self))nn def __bool__(self):n return bool(abs(self))nn @classmethodn def frombytes(cls, octets):n typecode = chr(octets[0])n memv = memoryview(octets[1:]).cast(typecode)n # 直接將memview傳給class即可,不用拆包n return cls(memv)n

這裡需要注意的就是__repr__的實現,

repr()函數的功能主要就是方便調試,

所以我們選擇把[ ]之外的自字元都去掉

協議和鴨子類型

在Python中創建功能完善的序列類型不需要使用繼承

只需實現符合序列協議的方法。

但這裡所說的協議究竟是什麼呢?

在面向對象的編程中,協議是非正式的介面

只在文檔中定義,在代碼中不定義。

例如:Python序列協議中 只需要有 __len__ 和 __getitem__兩個方法

任何的類 只要是使用用標準的簽名和語義實現了這兩個方法,就用在任何期待序列的地方。

還記得我們第一張寫的 FrenchDeck這個類么?

import collectionsnCard = collections.namedtuple(Card, [rank, suit])nclass FrenchDeck():n ranks = [str(n) for n in range(2, 11)] + list(JQKA) n suits = spades diamons clubs hearts.split()nn def __init__(self,):n self._cards = [Card(rank, suit) for suit in self.suitsn for rank in self.ranks]n n def __len__(self):n return len(self._cards)nn def __getitem__(self, position):n return self._cards[position]n

協議是非正式的,沒有強制性的

但,任何一個有經驗的Python程序員

都知道FrenchDeck類是一個序列類型的類。

因為這個類的行為和功能都像是一個序列

並且實現了序列類型的協議

這也就解釋了鴨子類型

當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,

那麼這隻鳥就可以被稱為鴨子

Vector第二版 可切片的Vector類

就和上文所說,

當我們實現了 __len__ 和 __getitem__這兩個方法

我們的向量類就開始向序列類轉換了。。。

class Vector:n 省略了n def __len__(self):n return len(self._components)nn def __getitem__(self,index):n return self._components[index]n n# 現在我們的向量類就已經支持切片操作了ntest = Vector([3,4,5])nprint(len(test))nprint(test[:2])nnnOUT:n3narray(d, [3.0, 4.0])nn

可以看到,我們已經實現了基本的切片功能

但這並不完美,

因為實際上進行切片的是Vector類內部數組_components

這樣就會損失大量的功能

下面我們來嘗試實現它

能處理切片的__getitem__方法

import numbersnn# 省略了很多ndef __getitem__(self, index):n cls = type(self) # 獲取實例的類型nn if isinstance(index, slice): # 如果index參數值是切片的對象n # 調用Vector的構造方法,建立一個新的切片後的Vector類n return cls(self._components[index])n elif isinstance(index, numbers.Integral): # 如果參數是整數類型n return self._components[index] # 我們就對數組進行切片n else: # 否則我們就拋出異常n msg = {cls.__name__} indices must be integersn raise TypeError(msg.format(cls=cls))n n# 現在我們的向量類就已經支持切片操作了ntest = Vector([3, 4, 5])nprint(test[-1])nprint(test[:2])nprint(test[1,2])nnnnOUT:n5.0n(3.0, 4.0)nTraceback (most recent call last):nTypeError: Vector indices must be integersnn

到這裡我們就實現了我們想要的功能。

Vector第三版 動態存取屬性

我們的向量類型已經不支持

通過訪問分量名來獲取屬性了。

想要獲取不同維度的屬性我們得這樣:

v = Vector([1,2,3])nv[0]nv[1]nv[2]n

但是我們想這樣獲取實例的前四個屬性

v = Vector([1,2,3,4])nv[x] # Vector(1.0)nv[y] # Vector(2.0)nv[z] # Vector(3.0)nv[t] # Vector(4.0) n

下面我們來實現這個功能

這裡主要就是添加一個 __getattr__方法

shortcut_names = xyztndef __getattr__(self, name):n cls = type(self) # 獲取類型n if len(name) == 1: # 判斷屬性名是否在我們定義的names中n pos = cls.shortcut_names.find(name)n if 0 <= pos < len(self._components):n return self._components[pos]n msg = {} objects has no attribute {}n raise AttributeError(msg.format(cls, name))n ntest = Vector([3, 4, 5])nprint(test.x)nprint(test.y)nprint(test.z)nprint(test.c)nnOUT:n3.0n4.0n5.0nTraceback (most recent call last):nAttributeError: <class __main__.Vector> objects has no attribute cnn

光能動態獲取屬性還不夠,

我們還想給這個向量類動態的存屬性值

def __setattr__(self, name, value):n cls = type(self)n if len(name) == 1: # 我們需要特別處理單個屬性名的情況n if name in cls.shortcut_names:n error = readonly attrbute {} # 當用戶想要重置xyzt的值時,拋出異常】n elif name.islower(): # 我們不想讓向量存在其他小寫字母的屬性n error = "cant set attributes a to z in {} "n else:n error = nn if error:n msg = error.format(cls.__name__, name)n raise AttributeError(msg)n super().__setattr__(name, value) # 沒有特殊情況我們調用超類的setattr來動態增加屬性nntest = Vector([3, 4, 5])nprint(test.x)ntest.x=1ntest.v=1ntest.A=1nprint(test.A)nnOUT:n3.0nAttributeError: readonly attrbute VectornAttributeError: cant set attributes a to z in Vectorn1nn

這裡有幾點我們需要注意:

  • 實現getattr方法之後,一般我們也要實現setattr
  • 實際上這裡的向量類還是不能改變的
  • 如果想要動態的改變這個向量類,我們需要實現__setitem__方法
  • 我們需要這個向量類不改變,因為我們要保證這個類是可散列

可散列和快速等值測試

我們想要保證Vector類是可散列的

那麼我們要實現__eq__ 和 __hash__ 方法

原本我們已經實現了__eq__的方法了,

但實際上我們是通過將數組轉為tuple

再通過tuple的eq方法來比較是否相等

這樣當向量的維度較少時還可以應付

但是當我們用(超維 或者n維向量時)

效率就太低下了,

所以我們用一個效率更高的的方法

def __eq__(self, other):n if len(self) != len(other): # 首先判斷長度是否相等n return Falsen for a, b in zip(self, other): # 接著逐一判斷每個元素是否相等 n if a != b:n return Falsen return Truenn# 我們也可以寫的漂亮點nndef __eq__(self, other):n return (len(self) == len(other)) and all(a == b for a, b in zip(self, other))n

接著 我們來實現__hash__方法

每次實現hash方法,我們都是要通過我們的老朋友(「」異或運算符)

這裡不同於二維向量,

我們需要給n維元素逐一進行異或運算

那麼reduce 高階函數就派上用場了

import functoolsnimport operatorndef __hash__(self):n # 先用生成器表達式惰性計算各個分量的散列值n hashes = (hash(x) for x in self._components)n # 再用過operator的xor函數 計算聚合的散列值 第三個參數0是初始值n return functools.reduce(operator.xor,hashes,0)n nv1 = Vector([3, 4])nv2 = Vector([2, 2])nv3 = Vector([3.0, 4.0])nprint(hash(v1))nprint(hash(v2))nprint(hash(v3))nprint(v1 == v2)nprint(v1 == v3)nnOUT:n7n0n7nFalsenTruenn

Vector類的格式化

想要格式化向量實例

這裡就得考慮到這個有可能是個多維向量

當維度超過四維之後,

我們就不能用普通的笛卡爾坐標體系去標記了

這裡我們引入 『超球體/n維球面』這一概念

n維球面是普通的球面在任意維度的推廣。它是(n + 1)維空間內的n維流形。特別地,0維球面就是直線上的兩個點,1維球面是平面上的圓,2維球面是三維空間內的普通球面。高於2維的球面有時稱為超球面。中心位於原點且半徑為單位長度的n維球面稱為單位n維球面

我們選擇用h來做這裡的 擴展格式規範微語言

這裡我們實現__format__方法

實現該方法之前,我們還需要定義兩個輔助方法

來幫助我們計算所有的教坐標

import itertoolsn def angle(self, n):n # 使用 n維度球體 中的公式計算某個角坐標n r = math.sqrt(sum(x * x for x in self[n:]))n a = math.atan2(r, self[n - 1])n if (n == len(self)) and (self[-1] < 0):n return math.pi * 2 - an else:n return ann def angles(self):n # 生成器表達式,計算所有的叫坐標n return (self.angle(n) for n in range(1,len(self)))nn def __format__(self, fmt_spec):n if fmt_spec.endswith(h): # 超球面坐標n fmt_spec = fmt_spec[:-1]n # 使用itertools.chain生成器表達式,無迭代向量的模和各個角坐標n coords = itertools.chain([abs(self)], self.angles())n out_fmt = <{}> # 球面坐標n else:n coords = selfn out_fmt = ({}) # 笛卡爾坐標n components = (format(c, fmt_spec) for c in coords) # 格式化各個元素n return out_fmt.format(, .join(components)) #返回我們需要的nn# testn v1 = Vector([1,1,2])nv2 = Vector([1,2,3,4,5,6])nprint(format(v1,.3f))nprint(format(v2,.3h))nnnOUT:n(1.000, 1.000, 2.000)n<9.54, 1.47, 1.36, 1.24, 1.1, 0.876>n n

到這裡我們的基本需求就全部實現了。

下面附上最終版本的代碼

from array import arraynimport reprlibnimport mathnimport numbersnimport functoolsnimport operatornimport itertoolsnnnclass Vector:n typecode = dn shortcut_names = xyztnn def __init__(self, components):n # 用 _ 代表受保護的屬性,把Vector的分量保存在一個數組之中n self._components = array(self.typecode, components)nn def __iter__(self):n # 通過_components 來構造一個迭代器n return iter(self._components)nn def __repr__(self):n # 使用reprlib.repr()函數來獲取分量的有限長度的表現形式(防止出現1000維度的實例顯示問題)n components = reprlib.repr(self._components)n # 通過字元串生成實例是 刪掉【 之前和array(d)後面的n components = components[components.find([):-1]n return Vector({}).format(components)nn def __str__(self):n return str(tuple(self))nn def __bytes__(self):n # 這裡我們可以直接使用_components 來構建bytes對象n return (bytes([ord(self.typecode)]) + bytes(self._components))n def __eq__(self, other):n return (len(self) == len(other)) and all(a == b for a, b in zip(self, other))nn def __hash__(self):n # 先用生成器表達式惰性計算各個分量的散列值n hashes = (hash(x) for x in self)n # 再用過operator的xor函數 計算聚合的散列值 第三個參數0是初始值n return functools.reduce(operator.xor, hashes, 0)nn def __abs__(self):n # 通過計算各個分量的平方和來算出絕對值(距離)n return math.sqrt(sum(x * x for x in self))nn def __bool__(self):n return bool(abs(self))nn def __len__(self):n return len(self._components)nn def __getitem__(self, index):n cls = type(self) # 獲取實例的類型nn if isinstance(index, slice): # 如果index參數值是切片的對象n # 調用Vector的構造方法,建立一個新的切片後的Vector類n return cls(self._components[index])n elif isinstance(index, numbers.Integral): # 如果參數是整數類型n return self._components[index] # 我們就對數組進行切片n else: # 否則我們就拋出異常n msg = {cls.__name__} indices must be integersn raise TypeError(msg.format(cls=cls))nn def __getattr__(self, name):n cls = type(self) # 獲取類型n if len(name) == 1: # 判斷屬性名是否在我們定義的names中n pos = cls.shortcut_names.find(name)n if 0 <= pos < len(self._components):n return self._components[pos]n msg = {} objects has no attribute {}n raise AttributeError(msg.format(cls, name))nn def __setattr__(self, name, value):n cls = type(self)n if len(name) == 1: # 我們需要特別處理單個屬性名的情況n if name in cls.shortcut_names:n error = readonly attrbute {} # 當用戶想要重置xyzt的值時,拋出異常】n elif name.islower(): # 我們不想讓向量存在其他小寫字母的屬性n error = "cant set attributes a to z in {} "n else:n error = nn if error:n msg = error.format(cls.__name__, name)n raise AttributeError(msg)n super().__setattr__(name, value) # 沒有特殊情況我們調用超類的setattr來動態增加屬性nn def angle(self, n):n # 使用 n維度球體 中的公式計算某個角坐標n r = math.sqrt(sum(x * x for x in self[n:]))n a = math.atan2(r, self[n - 1])n if (n == len(self)) and (self[-1] < 0):n return math.pi * 2 - an else:n return ann def angles(self):n # 生成器表達式,計算所有的叫坐標n return (self.angle(n) for n in range(1,len(self)))nn def __format__(self, fmt_spec):n if fmt_spec.endswith(h): # 超球面坐標n fmt_spec = fmt_spec[:-1]n # 使用itertools.chain生成器表達式,無迭代向量的模和各個角坐標n coords = itertools.chain([abs(self)], self.angles())n out_fmt = <{}> # 球面坐標n else:n coords = selfn out_fmt = ({}) # 笛卡爾坐標n components = (format(c, fmt_spec) for c in coords) # 格式化各個元素n return out_fmt.format(, .join(components)) #返回我們需要的nn @classmethodn def frombytes(cls, octets):n typecode = chr(octets[0])n memv = memoryview(octets[1:]).cast(typecode)n # 直接將memview傳給class即可,不用拆包n return cls(memv)n

本章小結

這一章我們依舊是通過不斷的為一個類加入特殊方法

來讓這個類的行為更像Python風格的標準類

與此同時我們還介紹了:

  • 鴨子類型
  • 序列的協議
  • 效率更高的比較方法(利用zip函數)
  • 更方便的異或計算函數(operator.xor)
  • ....

最後作者還告訴我們:

設計類的時候要遵循 KISS原則

Keep it simple , stupid 不要過度的遵循設計協議

PS : 關於超球體,說實在的我是不太能很好的理解,

如果大家覺得實現起來困難,也不要太過於在意。

每天的學習記錄都會 同步更新到:

微信公眾號: findyourownway

知乎專欄:zhuanlan.zhihu.com/zen-

Blog : www.ehcoblog.ml

Github: github.com/Ehco1996/

推薦閱讀:

【scikit-learn文檔解析】集成方法 Ensemble Methods(上):Bagging與隨機森林
用python多進程,fork()之後創建了新進程,原來上下文裡面的局部變數也會再創建值完全一樣的么?
Python練習第一題,在圖片上加入數字
Python篇-多進程與協程的理解與使用

TAG:Python | Python教程 |