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