第十章 序列的修散列和切片
第十章 序列的修散列和切片
本章將以第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
知乎專欄:https://zhuanlan.zhihu.com/zen-of-pythonBlog : www.ehcoblog.mlGithub: https://github.com/Ehco1996/推薦閱讀:
※【scikit-learn文檔解析】集成方法 Ensemble Methods(上):Bagging與隨機森林
※用python多進程,fork()之後創建了新進程,原來上下文裡面的局部變數也會再創建值完全一樣的么?
※Python練習第一題,在圖片上加入數字
※Python篇-多進程與協程的理解與使用