《流暢的Python》筆記。本篇是「面向對象慣用方法」的第三篇。本篇將以上一篇中的Vector2d爲基礎,定義多維向量Vector。python
自定義Vector
類的行爲將與Python標準中的不可變扁平序列同樣,它將支持以下功能:編程
__len__
和__getitem__
;Vector
實例;本篇還將經過__getattr__
方法實現屬性的動態存取(雖然序列類型一般不會這麼作),以及穿插討論一個概念:把協議當作正式接口。咱們將說明協議和鴨子類型之間的關係,以及對自定義類型的影響。bash
Vector
的構造方法將和全部內置序列類型同樣,以可迭代對象爲參數。若是其中元素過多,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
實現序列協議。ssh
協議和鴨子類型在以前的文章中也有所說起。在面向對象編程中,協議是非正式的接口,只在文檔中定義,在代碼中不定義。函數
在Python中,只要實現了協議須要的某些方法,其實就算實現了協議,而不必定須要繼承。好比只要實現了__len__
和__getitem__
這兩個方法,那麼這個類就是知足序列協議的,而不須要從什麼「序列基類」繼承。測試
鴨子類型:和現實中相反,Python中肯定一個東西是否是「鴨子」,不是測它的「DNA」是否是」鴨子「的DNA,而是看這東西像不像只鴨子。只要像」鴨子「,那它就是「鴨子」。好比,只要一個類實現了__len__
和__getitem__
方法,那它就是序列類,而沒必要管它是從哪來的;文件類對象也常是鴨子類型。網站
讓Vector
變爲序列類型,並能正確返回切片:spa
# 代碼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
類型。正是爲了使切片的類型正確,這裏才作了類型判斷。.net
上述代碼中用到了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
方法將爲你節省大量時間。
目前版本的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]) # 行爲不一致
__slots__
來禁止添加屬性,但咱們這裏仍然選擇實現__setattr__
來實現此功能。__slots__
屬性最好只用於節省內存,並且僅在內存嚴重不足時才用它,別爲了秀操做而寫一些別人看着很彆扭的代碼(只寫給本身看的除外)。目前這個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),這個小物件把兩個拉鍊條的鏈牙要合在一塊兒,是否是很形象?
Vector2d
中,當傳入'p'
時,以極座標的形式格式化數據;因爲Vector
的維度可能大於2,如今,當傳入參數'h'
時,咱們使用球面座標格式化數據,即'<r, Φ1, Φ2, Φ3>'
。同時,還須要定義兩個輔助方法:
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 ~