摘要:具體方法和上一篇一樣,也是用各個分量的哈希值進行異或運算,由于的分量可能很多,這里我們使用函數來歸約異或值。每個分量被映射成了它們的哈希值,這些哈希值再歸約成一個值這里的傳入了第三個參數,并且建議最好傳入第三個參數。
《流暢的Python》筆記。1. 前言本篇是“面向對象慣用方法”的第三篇。本篇將以上一篇中的Vector2d為基礎,定義多維向量Vector。
自定義Vector類的行為將與Python標準中的不可變扁平序列一樣,它將支持如下功能:
基本的序列協議:__len__和__getitem__;
正確表述擁有很多元素的實例;
適當的切片支持,用于生成新的Vector實例;
綜合各個元素的值計算散列值;
自定義的格式語言擴展。
本篇還將通過__getattr__方法實現屬性的動態存取(雖然序列類型通常不會這么做),以及穿插討論一個概念:把協議當做正式接口。我們將說明協議和鴨子類型之間的關系,以及對自定義類型的影響。
2. 初版VectorVector的構造方法將和所有內置序列類型一樣,以可迭代對象為參數。如果其中元素過多,repr()函數返回的字符串將會使用...省略一部分內容,它的初始版本如下:
# 代碼1 from array import array import reprlib import math class Vector: typecode = "d" def __init__(self, components): # 以可迭代對象為參數 self._components = array(self.typecode, components) def __iter__(self): return iter(self._components) def __repr__(self): components = reprlib.repr(self._components) components = components[components.find("["):-1] return "Vector({})".format(components) def __str__(self): # 和Vector2d相同 return str(tuple(self)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(self._components)) def __eq__(self, other): # 和Vector2d相同 return tuple(self) == tuple(other) def __abs__(self): return math.sqrt(sum(x * x for x in self)) def __bool__(self): # 和Vector2d相同 return bool(abs(self)) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(memv) # 去掉了Vector2d中的星號*
之所以沒有直接繼承制Vector2d,既是因為這兩個類的構造方法不兼容,也是因為我們要為Vector實現序列協議。
3. 協議和鴨子類型協議和鴨子類型在之前的文章中也有所提及。在面向對象編程中,協議是非正式的接口,只在文檔中定義,在代碼中不定義。
在Python中,只要實現了協議需要的某些方法,其實就算實現了協議,而不一定需要繼承。比如只要實現了__len__和__getitem__這兩個方法,那么這個類就是滿足序列協議的,而不需要從什么“序列基類”繼承。
鴨子類型:和現實中相反,Python中確定一個東西是不是“鴨子”,不是測它的“DNA”是不是”鴨子“的DNA,而是看這東西像不像只鴨子。只要像”鴨子“,那它就是“鴨子”。比如,只要一個類實現了__len__和__getitem__方法,那它就是序列類,而不必管它是從哪來的;文件類對象也常是鴨子類型。
4. 第2版Vector:支持切片讓Vector變為序列類型,并能正確返回切片:
# 代碼2,將以下代碼添加到初版Vector中 class Vector: -- snip -- def __len__(self): return len(self._components) def __getitem__(self, index): cls = type(self) if isinstance(index, slice): # 如果index是個切片類型,則構造新實例 return cls(self._components[index]) elif isinstance(index, numbers.Integral): # 如果index是個數,則直接返回 return self._components[index] else: msg = "{cls.__name__} indices must be integers" raise TypeError(msg.format(cls=cls))
如果__getitem__函數直接返回切片:return self._components[index],那么得到的數據將是array類型,而不是Vector類型。正是為了使切片的類型正確,這里才做了類型判斷。
上述代碼中用到了slice類型,它是Python的內置類型,這里順便補充一下切片原理,直接上代碼:
# 代碼3 >>> class MySeq: ... def __getitem__(self, index): ... return index # 直接返回傳給它的值 ... >>> s = MySeq() >>> s[1] 1 # 單索引,沒啥新奇的 >>> s[1:3] slice(1, 3, None) # 返回來一個slice類型 >>> s[1:10:2] slice(1, 10, 2) # 注意slice類型的結構 >>> s[1:10:2, 9] (slice(1, 10, 2), 9) # 如果[]中有逗號,__getitem__收到的是元組 >>> s[1:10:2, 7:9] (slice(1, 10, 2), slice(7, 9, None)) >>> dir(slice) # 注意最后四個元素 ["__class__", "__delattr__", "__dir__", "__doc__", "__eq__", "__format__", "__ge__", "__getattribute__", "__gt__", "__hash__", "__init__", "__init_subclass__", "__le__", "__lt__", "__ne__", "__new__", "__reduce__", "__reduce_ex__", "__repr__", "__setattr__", "__sizeof__", "__str__", "__subclasshook__", "indices", "start", "step", "stop"]
當我們用dir()函數獲取slice的屬性時,發現它有start,stop和step數據屬性,并且還有一個indices方法,這里重點說說這個indices方法。它接收一個長度參數len,并根據這個len將slice類型的start,stop和step三個參數正確轉換成在長度范圍內的非負數,具體用法如下:
# 代碼4 >>> slice(None, 10, 2).indices(5) (0, 5, 2) # 將這些煩人的索引統統轉換成明確的正向索引 >>> slice(-3, None, None).indices(5) (2, 5, 1)
自定義Vector類中并沒有使用這個方法,因為Vector的底層我們使用了array.array數據類型,切片的具體操作不用我們自行編寫。但如果你的類沒有這樣的底層序列類型做支撐,那么slice.indices方法將為你節省大量時間。
5. 第3版Vector:動態存儲屬性目前版本的Vector中,沒有辦法通過名稱訪問向量的分量(如v.x和v.y),而且現在的Vector可能存在大量分量。不過,如果能通過單個字母訪問前幾個分量的話,這樣將很方便,也更人性化。現在,我們想用x,y,z,t四個字母分別代替v[0],v[1],v[2]和v[3],但具體做法并不是為實例添加這四個屬性,并且我們也不想在運行時實例能動態添加單個字母的屬性,更不想實例能通過這四個字母修改Vector中self._components的值。換句話說,我們只想通過這四個字母提供一種較為方便的訪問方式,僅此而已。而要實現這樣的功能,則需要實現__getattr__和__setattr__方法,以下是它們的代碼:
# 代碼5.1 class Vector: -- snip -- shortcut_name = "xyzt" def __getattr__(self, name): cls = type(self) if len(name) == 1: # 如果屬性是單個字母 pos = cls.shortcut_name.find(name) if 0 <= pos < len(self._components): # 判斷是不是xyzt中的一個 return self._components[pos] msg = "{.__name__!r} object has no attribute {!r}" # 想要獲取其他屬性時則拋出異常 raise AttributeError(msg.format(cls, name)) def __setattr__(self, name, value): cls = type(self) if len(name) == 1: # 不允許創建單字母實例屬性,即便是x,y,z,t if name in cls.shortcut_name: # 如果name是xyzt中的一個,設置特殊的錯誤信息 error = "readonly attibute {attr_name!r}" elif name.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=name) raise AttributeError(msg) super().__setattr__(name, value)
解釋:
屬性查找失敗后,解釋器會調用__getattr__方法。簡單來說,對my_obj.x表達式,Python會檢查my_obj實例有沒有名為x的實例屬性;如果沒有,則到它所屬的類中查找有沒有名為x的類屬性;如果還是沒有,則順著繼承樹繼續查找。如果依然找不到,則會調用my_obj所屬類中定義的__getattr__方法,傳入self和屬性名的字符串形式(如"x");
__getattr__和__setattr_方法一般同時定義,否則對象的行為很容易出現不一致。比如,如果這里只定義__getattr__方法,則會出現如下尷尬的代碼:
# 代碼5.2 >>> v = Vector(range(5)) >>> v Vector([0.0, 1.0, 2.0, 3.0, 4.0]) >>> v.x 0.0 >>> v.x = 10 # 按理說這里應該報錯才對,因為不允許修改 >>> v.x 10 >>> v # 其實是v創建了新實例屬性x,這也是為什么我們要定義__setattr__ Vector([0.0, 1.0, 2.0, 3.0, 4.0]) # 行為不一致
我們沒有禁止動態添加屬性,只是禁止為單個字母屬性賦值,如果屬性名的長度大于1,這樣的屬性是可以動態添加的;
如果你看過上一篇文章,那么你可能會想到用__slots__來禁止添加屬性,但我們這里仍然選擇實現__setattr__來實現此功能。__slots__屬性最好只用于節省內存,而且僅在內存嚴重不足時才用它,別為了秀操作而寫一些別人看著很別扭的代碼(只寫給自己看的除外)。
6. 第4版Vector:散列和快速等值測試目前這個Vector是不可散列的,現在我們來實現__hash__方法。具體方法和上一篇一樣,也是用各個分量的哈希值進行異或運算,由于Vector的分量可能很多,這里我們使用functools.reduce函數來歸約異或值。同時,我們還將改寫之前那個簡潔版的__eq__,使其更高效(至少對大型向量來說更高效):
# 代碼6,請自行導入所需的模塊 class Vector: -- snip -- def __hash__(self): hashs = (hash(x) for x in self._components) # 先求各個分量的哈希值 return functools.reduce(operator.xor, hashs, 0) # 然后將所有哈希值歸約成一個值 def __eq__(self, other): # 不用像之前那樣:生成元組只為使用元組的__eq__方法 return len(self) == len(self) and all(a == b for a, b in zip(self, other))
解釋:
此處的__hash__方法實際上執行的是一個映射歸約的過程。每個分量被映射成了它們的哈希值,這些哈希值再歸約成一個值;
這里的functool.reduce傳入了第三個參數,并且建議最好傳入第三個參數。傳入第三個參數能避免這個異常:TypeError: reduce() of empty sequence with no initial value。如果序列為空,第三個參數就是返回值;否則,在歸約中它將作為第一個參數;
在__eq__方法中先比較兩序列的長度并不僅僅是一種捷徑。zip函數并行遍歷多個可迭代對象,如果其中一個耗盡,它會立即停止生成值,而且不發出警告;
補充一個小知識:zip函數和文件壓縮沒有關系,它的名字取自拉鏈頭(zipper fastener),這個小物件把兩個拉鏈條的鏈牙要合在一起,是不是很形象?7. 第5版Vector:格式化
Vector2d中,當傳入"p"時,以極坐標的形式格式化數據;由于Vector的維度可能大于2,現在,當傳入參數"h"時,我們使用球面坐標格式化數據,即"
angle(n),用于計算某個角坐標;
angles(),返回由所有角坐標構成的可迭代對象。
至于這兩個的數學原理就不解釋了。以下是最后要添加的代碼:
# 代碼7 class Vector: -- snip -- 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 return a def angles(self): return (self.angle(n) for n in range(1, len(self))) def __format__(self, format_spec=""): if format_spec.endswith("h"): # 如果格式說明符以"h"結尾 format_spec = format_spec[:-1] # 格式說明符前面部分保持不變 coords = itertools.chain([abs(self)], self.angles()) # outer_fmt = "<{}>" else: coords = self outer_fmt = "({})" components = (format(c, format_spec) for c in coords) return outer_fmt.format(", ".join(components))
itertools.chain函數生成生成器表達式,將多個可迭代對象連接成在一起進行迭代。關于生成器的更多內容將在以后的文章中介紹。
至此,多維Vector暫時告一段落。
迎大家關注我的微信公眾號"代碼港" & 個人網站 www.vpointer.net ~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/41895.html
摘要:一基本的序列協議首先,需要就維向量和二維向量的顯示模的計算等差異重新調整。假設維向量最多能處理維向量,訪問向量分量的代碼實現如下若傳入的參數在備選分量中可進行后續處理判斷分量的位置索引是否超出實例的邊界不支持非法的分量訪問,拋出。 導語:本文章記錄了本人在學習Python基礎之面向對象篇的重點知識及個人心得,打算入門Python的朋友們可以來一起學習并交流。 本文重點: 1、了解協議的...
摘要:例如,的序列協議只需要和兩個方法。任何類如,只要使用標準的簽名和語義實現了這兩個方法,就能用在任何期待序列的地方。方法開放了內置序列實現的棘手邏輯,用于優雅地處理缺失索引和負數索引,以及長度超過目標序列的切片。 序列的修改、散列和切片 接著造Vector2d類 要達到的要求 為了編寫Vector(3, 4) 和 Vector(3, 4, 5) 這樣的代碼,我們可以讓 init 法接受任...
摘要:導語本文章匯總了本人在學習基礎之緒論篇數據結構篇函數篇面向對象篇控制流程篇和元編程篇學習筆記的鏈接,打算入門的朋友們可以按需查看并交流。 導語:本文章匯總了本人在學習Python基礎之緒論篇、數據結構篇、函數篇、面向對象篇、控制流程篇和元編程篇學習筆記的鏈接,打算入門Python的朋友們可以按需查看并交流。 第一部分:緒論篇 1、Python數據模型 第二部分:數據結構篇 2、序列構成...
摘要:本篇繼續學習之路,實現更多的特殊方法以讓自定義類的行為跟真正的對象一樣。之所以要讓向量不可變,是因為我們在計算向量的哈希值時需要用到和的哈希值,如果這兩個值可變,那向量的哈希值就能隨時變化,這將不是一個可散列的對象。 《流暢的Python》筆記。本篇是面向對象慣用方法的第二篇。前一篇講的是內置對象的結構和行為,本篇則是自定義對象。本篇繼續Python學習之路20,實現更多的特殊方法以讓...
閱讀 2753·2021-11-19 09:40
閱讀 5321·2021-09-27 14:10
閱讀 2108·2021-09-04 16:45
閱讀 1480·2021-07-25 21:37
閱讀 3003·2019-08-30 10:57
閱讀 2988·2019-08-28 17:59
閱讀 1062·2019-08-26 13:46
閱讀 1414·2019-08-26 13:27