摘要:本篇繼續(xù)學(xué)習(xí)之路,實(shí)現(xiàn)更多的特殊方法以讓自定義類的行為跟真正的對(duì)象一樣。之所以要讓向量不可變,是因?yàn)槲覀冊(cè)谟?jì)算向量的哈希值時(shí)需要用到和的哈希值,如果這兩個(gè)值可變,那向量的哈希值就能隨時(shí)變化,這將不是一個(gè)可散列的對(duì)象。
《流暢的Python》筆記。1. 前言
本篇是“面向?qū)ο髴T用方法”的第二篇。前一篇講的是內(nèi)置對(duì)象的結(jié)構(gòu)和行為,本篇?jiǎng)t是自定義對(duì)象。本篇繼續(xù)“Python學(xué)習(xí)之路20”,實(shí)現(xiàn)更多的特殊方法以讓自定義類的行為跟真正的Python對(duì)象一樣。
本篇要討論的內(nèi)容如下,重點(diǎn)放在了對(duì)象的各種輸出形式上:
實(shí)現(xiàn)用于生成對(duì)象其他表示形式的內(nèi)置函數(shù)(如repr(),bytes()等);
使用一個(gè)類方法實(shí)現(xiàn)備選構(gòu)造方法;
擴(kuò)展內(nèi)置的format()函數(shù)和str.format()方法使用的格式微語言;
實(shí)現(xiàn)只讀屬性;
實(shí)現(xiàn)對(duì)象的可散列;
利用__slots__節(jié)省內(nèi)存;
如何以及何時(shí)使用@classmethod和@staticmethd裝飾器;
Python的私有屬性和受保護(hù)屬性的用法、約定和局限。
本篇將通過實(shí)現(xiàn)一個(gè)簡(jiǎn)單的二維歐幾里得向量類型,來涵蓋上述內(nèi)容。
不過在開始之前,我們需要補(bǔ)充幾個(gè)概念:
repr():以便于開發(fā)者理解的方式返回對(duì)象的字符串表示形式,它調(diào)用對(duì)象的__repr__特殊方法;
str():以便于用戶理解的方式返回對(duì)象的字符串表示形式,它調(diào)用對(duì)象的__str__特殊方法;
bytes():獲取對(duì)象的字節(jié)序列表示形式,它調(diào)用對(duì)象的__bytes__特殊方法;
format()和str.format():格式化輸出對(duì)象的字符串表示形式,調(diào)用對(duì)象的__format__特殊方法。
2. 自定義向量類Vector2d我們希望這個(gè)類具備如下行為:
# 代碼1 >>> v1 = Vector2d(3, 4) >>> print(v1.x, v1.y) # Vector2d實(shí)例的分量可直接通過實(shí)例屬性訪問,無需調(diào)用讀值方法 3.0 4.0 >>> x, y = v1 # 實(shí)例可拆包成變量元組 >>> x, y (3.0, 4.0) >>> v1 # 我們希望__repr__返回的結(jié)果類似于構(gòu)造實(shí)例的源碼 Vector2d(3.0, 4.0) >>> v1_clone = eval(repr(v1)) # 只是為了說明repr()返回的結(jié)果能用來生成實(shí)例 >>> v1 == v1_clone # Vector2d需支持 == 運(yùn)算符 True >>> print(v1) # 我們希望__str__方法以如下形式返回實(shí)例的字符串表示 (3.0, 4.0) >>> octets = bytes(v1) # 能夠生成字節(jié)序列 >>> octets b"dx00x00x00x00x00x00x08@x00x00x00x00x00x00x10@" >>> abs(v1) # 能夠求模 5.0 >>> bool(v1), bool(Vector2d(0, 0)) # 能進(jìn)行布爾運(yùn)算 (True, False)
Vector2d的初始版本如下:
# 代碼2 from array import array import math class Vector2d: # 類屬性,在Vector2d實(shí)例和字節(jié)序列之間轉(zhuǎn)換時(shí)使用 typecode = "d" # 轉(zhuǎn)換成C語言中的double類型 def __init__(self, x, y): self.x = float(x) # 構(gòu)造是就轉(zhuǎn)換成浮點(diǎn)數(shù),盡早在構(gòu)造階段就捕獲錯(cuò)誤 self.y = float(y) def __iter__(self): # 將Vector2d實(shí)例變?yōu)榭傻鷮?duì)象 return (i for i in (self.x, self.y)) # 這是生成器表達(dá)式! def __repr__(self): class_name = type(self).__name__ # 獲取類名,沒有采用硬編碼 # 由于Vector2d實(shí)例是可迭代對(duì)象,所以*self會(huì)把x和y提供給format函數(shù) return "{}({!r}, {!r})".format(class_name, *self) def __str__(self): return str(tuple(self)) # 由可迭代對(duì)象構(gòu)造元組 def __bytes__(self): # ord()返回字符的Unicode碼位;array中的數(shù)組的元素是double類型 return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))) def __eq__(self, other): # 這樣實(shí)現(xiàn)有缺陷,Vector(3, 4) == [3, 4]也會(huì)返回True return tuple(self) == tuple(other) # 但這個(gè)缺陷會(huì)在后面章節(jié)修復(fù) def __abs__(self): # 計(jì)算平方和的非負(fù)數(shù)根 return math.hypot(self.x, self.y) def __bool__(self): # 用到了上面的__abs__來計(jì)算模,如果模為0,則是False,否則為True return bool(abs(self))3. 備選構(gòu)造方法
初版Vector2d可將它的實(shí)例轉(zhuǎn)換成字節(jié)序列,但卻不能從字節(jié)序列構(gòu)造Vector2d實(shí)例,下面添加一個(gè)方法實(shí)現(xiàn)此功能:
# 代碼3 class Vector2d: -- snip -- @classmethod def frombytes(cls, octets): # 不用傳入self參數(shù),但要通過cls傳入類本身 typecode = chr(octets[0]) # 從第一個(gè)字節(jié)中讀取typecode,chr()將Unicode碼位轉(zhuǎn)換成字符 # 使用傳入的octets字節(jié)序列構(gòu)建一個(gè)memoryview,然后根據(jù)typecode轉(zhuǎn)換成所需要的數(shù)據(jù)類型 memv = memoryview(octets[1:]).cast(typecode) return cls(*memv) # 拆包轉(zhuǎn)換后的memoryview,然后構(gòu)造一個(gè)Vector2d實(shí)例,并返回4. classmethod與staticmethod
代碼3中用到了@classmethod裝飾器,與它相伴的還有@staticmethod裝飾器。
從上述代碼可以看出,classmethod定義的是傳入類而不是傳入實(shí)例的方法,即傳入的第一個(gè)參數(shù)必須是類,而不是實(shí)例。classmethod改變了調(diào)用方法的方式,但是,在實(shí)際調(diào)用這個(gè)方法時(shí),我們不需要手動(dòng)傳入cls這個(gè)參數(shù),Python會(huì)自動(dòng)傳入。(按照傳統(tǒng),第一個(gè)參數(shù)一般命名為cls,當(dāng)然你也可以另起名)
staticmethod也會(huì)改變方法的調(diào)用方式,但第一個(gè)參數(shù)不是特殊值,既不是cls,也不是self,就是用戶傳入的普通參數(shù)。以下是它們的用法對(duì)比:
# 代碼4 >>> class Demo: ... @classmethod ... def klassmeth(*args): ... return args # 返回傳入的全部參數(shù) ... @staticmethod ... def statmeth(*args): ... return args # 返回傳入的全部參數(shù) ... >>> Demo.klassmeth() (,) # 不管如何調(diào)用Demo.klassmeth,它的第一個(gè)參數(shù)始終是Demo類自己 >>> Demo.klassmeth("spam") ( , "spam") >>> Demo.statmeth() () # Demo.statmeth的行為與普通函數(shù)類似 >>> Demo.statmeth("spam") ("spam",)
classmethod很有用,但staticmethod一般都能找到很方便的替代方案,所以staticmethod并不是必須的。
5. 格式化顯示內(nèi)置的format()函數(shù)和str.format()方法把各個(gè)類型的格式化方式委托給相應(yīng)的.__format__(format_spec)方法。format_spec是格式說明符,它是:
format(my_obj, format_spec)的第二個(gè)參數(shù);
也是str.format()方法的格式字符串,{}里替換字段中冒號(hào)后面的部分,例如:
# 代碼5 >>> brl = 1 / 2.43 >>> "1 BRL = {rate:0.2f} USD".format(rate=brl) # 此時(shí) format_spec為"0.2f"
其中,冒號(hào)后面的0.2f是格式說明符,冒號(hào)前面的rate是字段名稱,與格式說明符無關(guān)。格式說明符使用的表示法叫格式規(guī)范微語言(Format Specification Mini-Language)。格式規(guī)范微語言為一些內(nèi)置類型提供了專門的表示代碼,比如b表示二進(jìn)制的int類型;同時(shí)它還是可擴(kuò)展的,各個(gè)類可以自行決定如何解釋format_spec參數(shù),比如時(shí)間的轉(zhuǎn)換格式%H:%M:%S,就可用于datetime類型,但用于int類型則可能報(bào)錯(cuò)。
如果類沒有定義__format__方法,則會(huì)返回__str__的結(jié)果,比如我們定義的Vector2d類型就沒有定義__format__方法,但依然可以調(diào)用format()函數(shù):
# 代碼6 >>> v1 = Vector2d(3, 4) >>> format(v1) "(3.0, 4.0)"
但現(xiàn)在的Vector2d在格式化顯示上還有缺陷,不能向format()傳入格式說明符:
>>> format(v1, ".3f") Traceback (most recent call last): -- snip -- TypeError: non-empty format string passed to object.__format__
現(xiàn)在我們來為它定義__format__方法。添加自定義的格式代碼,如果格式說明符以"p"結(jié)尾,則以極坐標(biāo)的形式輸出向量,即
# 代碼7 class Vector2d: -- snip -- def angle(self): return math.atan2(self.y, self.x) # 弧度 def __format__(self, format_spec=""): if format_spec.endswith("p"): format_spec = format_spec[:-1] coords = (abs(self), self.angle()) outer_fmt = "<{}, {}>" else: coords = self outer_fmt = "({}, {})" components = (format(c, format_spec) for c in coords) return outer_fmt.format(*components)
以下是實(shí)際示例:
# 代碼8 >>> format(Vector2d(1, 1), "0.5fp") "<1.41421, 0.78540>" >>> format(Vector2d(1, 1), "0.5f") "(1.00000, 1.00000)"6. 可散列的Vector2d
關(guān)于可散列的概念可以參考之前的文章《Python學(xué)習(xí)之路22》。
目前的Vector2d是不可散列的,為此我們需要實(shí)現(xiàn)__hash__特殊方法,而在此之前,我們還要讓向量不可變,即self.x和self.y的值不能被修改。之所以要讓向量不可變,是因?yàn)槲覀冊(cè)谟?jì)算向量的哈希值時(shí)需要用到self.x和self.y的哈希值,如果這兩個(gè)值可變,那向量的哈希值就能隨時(shí)變化,這將不是一個(gè)可散列的對(duì)象。
補(bǔ)充:
在文章《Python學(xué)習(xí)之路22》中說道,用戶自定義的對(duì)象默認(rèn)是可散列的,它的散列值等于id()的返回值。但是此處的Vector2d卻是不可散列的,這是為什么?其實(shí),如果我們要讓自定義類變?yōu)榭缮⒘械模_的做法是同時(shí)實(shí)現(xiàn)__hash__和__eq__這兩個(gè)特殊方法。當(dāng)這兩個(gè)方法都沒有重寫時(shí),自定義類的哈希值就是id()的返回值,此時(shí)自定義類可散列;當(dāng)我們只重寫了__hash__方法時(shí),自定義類也是可散列的,哈希值就是__hash__的返回值;但是,如果只重寫了__eq__方法,而沒有重寫__hash__方法,此時(shí)自定義類便不可散列。
這里再次給出可散列對(duì)象必須滿足的三個(gè)條件:
支持hash()函數(shù),并且通過__hash__方法所得到的哈希值是不變的;
支持通過__eq__方法來檢測(cè)相等性;
若a == b為真,則hash(a) == hash(b)也必須為真。
根據(jù)官方文檔,最好使用異或運(yùn)算^混合各分量的哈希值,下面是Vector2d的改進(jìn):
# 代碼9 class Vector2d: -- snip -- def __init__(self, x, y): self.__x = float(x) self.__y = float(y) @property # 把方法變?yōu)閷傩哉{(diào)用,相當(dāng)于getter方法 def x(self): return self.__x @property def y(self): return self.__y def __hash__(self): return hash(self.x) ^ hash(self.y) -- snip --
文章至此說的都是一些特殊方法,如果想到得到功能完善的對(duì)象,這些方法可能是必備的,但如果你的應(yīng)用用不到這些東西,則完全沒有必要去實(shí)現(xiàn)這些方法,客戶并不關(guān)心你的對(duì)象是否符合Python風(fēng)格。
Vector2d暫時(shí)告一段落,現(xiàn)在來說一說其它比較雜的內(nèi)容。
7. Python的私有屬性和"受保護(hù)的"屬性Python不像C++、Java那樣可以用private關(guān)鍵字來創(chuàng)建私有屬性,但在Python中,可以以雙下劃線開頭來命名屬性以實(shí)現(xiàn)"私有"屬性,但是這種屬性會(huì)發(fā)生名稱改寫(name mangling):Python會(huì)在這樣的屬性前面加上一個(gè)下劃線和類名,然后再存入實(shí)例的__dict__屬性中,以最新的Vector2d為例:
# 代碼10 >>> v1 = Vector2d(1, 2) >>> v1.__dict__ {"_Vector2d__x": 1.0, "_Vector2d__y": 2.0}
當(dāng)屬性以雙下劃線開頭時(shí),其實(shí)是告訴別的程序員,不要直接訪問這個(gè)屬性,它是私有的。名稱改寫的目的是避免意外訪問,而不能防止故意訪問。只要你知道規(guī)則,這些屬性一樣可以訪問。
還有以單下劃線開頭的屬性,這種屬性在Python的官方文檔的某個(gè)角落里被稱為了"受保護(hù)的"屬性,但Python不會(huì)對(duì)這種屬性做特殊處理,這只是一種約定俗成的規(guī)矩,告訴別的程序員不要試圖從外部訪問這些屬性。這種命名方式很常見,但其實(shí)很少有人把這種屬性叫做"受保護(hù)的"屬性。
還是那句話,Python中所有的屬性都是公有的,Python沒有不能訪問的屬性!這些規(guī)則并不能阻止你有意訪問這些屬性,一切都看你遵不遵守上面這些"不成文"的規(guī)則了。
8. 覆蓋類屬性這里首先需要區(qū)分兩個(gè)概念,類屬性與實(shí)例屬性:
類屬性屬于整個(gè)類,該類的所有實(shí)例都能訪問這個(gè)屬性,可以動(dòng)態(tài)綁定類屬性,動(dòng)態(tài)綁定的類屬性所有實(shí)例也都可以訪問,即類屬性的作用域是整個(gè)類。可以按Vector2d中定義typecode的方式來定義類屬性,即直接在class中定義屬性,而不是在__init__中;
實(shí)例屬性只屬于某個(gè)實(shí)例對(duì)象,實(shí)例也能動(dòng)態(tài)綁定屬性。實(shí)例屬性只能這個(gè)實(shí)例自己訪問,即實(shí)例屬性的作用域是類對(duì)象作用域。實(shí)例屬性需要和self綁定,self指向的是實(shí)例,而不是類。
Python有個(gè)很獨(dú)特的特性:類屬性可用于為實(shí)例屬性提供默認(rèn)值。
Vector2d中有個(gè)typecode類屬性,注意到,我們?cè)?b>__bytes__方法中通過self.typecode兩次用到了它,這里明明是通過self調(diào)用實(shí)例屬性,可Vector2d的實(shí)例并沒有這個(gè)屬性。self.typecode其實(shí)獲取的是Vector2d.typecode類屬性的值,而至于怎么從實(shí)例屬性跳到類屬性的,以后有機(jī)會(huì)多帶帶用一篇文章來講。
補(bǔ)充:證明實(shí)例沒有typecode屬性
# 代碼11 >>> v = Vector2d(1, 2) >>> v.__dict__ {"_Vector2d__x": 1.0, "_Vector2d__y": 2.0} # 實(shí)例中并沒有typecode屬性
如果為不存在的實(shí)例屬性賦值,則會(huì)新建該實(shí)例屬性。假如我們?yōu)?b>typecode實(shí)例屬性賦值,同名類屬性不會(huì)受到影響,但會(huì)被實(shí)例屬性給覆蓋掉(類似于之前在函數(shù)閉包中講的局部變量和全局變量的區(qū)別)。借助這一特性,可以為各個(gè)實(shí)例的typecode屬性定制不同的值,比如在生成字節(jié)序列時(shí),將實(shí)例轉(zhuǎn)換成4字節(jié)的單精度浮點(diǎn)數(shù):
# 代碼12 >>> v1 = Vector2d(1.1, 2.2) >>> dumpd = bytes(v1) # 按雙精度轉(zhuǎn)換 >>> dumpd b"dx9ax99x99x99x99x99xf1?x9ax99x99x99x99x99x01@" >>> len(dumpd) 17 >>> v1.typecode = "f" >>> dumpf = bytes(v1) # 按單精度轉(zhuǎn)換 >>> dumpf b"fxcdxccx8c?xcdxccx0c@" # 明白為什么要在字節(jié)序列前加上typecode的值了嗎?為了支持不同格式。 >>> len(dumpf) 9 >>> Vector2d.typecode "d"
如果想要修改類屬性的值,必須直接在類上修改,不能通過實(shí)例修改。如果想修改所有實(shí)例的typecode屬性的默認(rèn)值,可以這么做:
# 代碼13 Vector2d.typecode = "f"
然而有種方式更符合Python風(fēng)格,而且效果持久,也更有針對(duì)性。通過繼承的方式修改類屬性,生成專門的子類。Django基于類的視圖就大量使用了這個(gè)技術(shù):
# 代碼14 >>> class ShortVector2d(Vector2d): ... typecode = "f" # 只修改這一處 ... >>> sv = ShortVector2d(1/11, 1/27) >>> sv ShortVector2d(0.09090909090909091, 0.037037037037037035) # 沒有硬編碼class_name的原因 >>> len(bytes(sv)) 99. __slots__類屬性
默認(rèn)情況下,Python在各個(gè)實(shí)例的__dict__屬性中以映射類型存儲(chǔ)實(shí)例屬性。正如《Python學(xué)習(xí)之路22》中所述,為了使用底層的散列表提升訪問速度,字典會(huì)消耗大量?jī)?nèi)存。如果要處理數(shù)百萬個(gè)屬性不多的實(shí)例,其實(shí)可以通過__slots__類屬性來節(jié)省大量?jī)?nèi)存。做法是讓解釋器用類似元組的結(jié)構(gòu)存儲(chǔ)實(shí)例屬性,而不是字典。
具體用法是,在類中創(chuàng)建這個(gè)__slots__類屬性,并把它的值設(shè)為一個(gè)可迭代對(duì)象,其中的元素是其余實(shí)例屬性的字符串表示。比如我們將之前定義的Vector2d改為__slots__版本:
# 代碼15 class Vector2d: __slots__ = ("__x", "__y") typecode = "d" # 其余保持不變 -- snip --
試驗(yàn)表明,創(chuàng)建一千萬個(gè)之前版本的Vector2d實(shí)例,內(nèi)存用量高達(dá)1.5GB,而__slots__版本的Vector2d的內(nèi)存用量不到700MB,并且速度也比之前的版本快。
但__slots__也有一些需要注意的點(diǎn):
使用__slots__之后,實(shí)例不能再有__slots__中所列名稱之外的屬性,即,不能動(dòng)態(tài)添加屬性;如果要使其能動(dòng)態(tài)添加屬性,必須在其中加入"__dict__",但這么做又違背了初衷;
每個(gè)子類都要定義__slots__屬性,解釋器會(huì)忽略掉父類的__slots__屬性;
自定義類中默認(rèn)有__weakref__屬性,但如果定義了__slots__屬性,而且還要自定義類支持弱引用,則需要把"__weakref__"加入到__slots__中。
總之,不要濫用__slots__屬性,也不要用它來限制用戶動(dòng)態(tài)添加屬性(除非有意為之)。__slots__在處理列表數(shù)據(jù)時(shí)最有用,例如模式固定的數(shù)據(jù)庫(kù)記錄,以及特大型數(shù)據(jù)集。然而,當(dāng)遇到這類數(shù)據(jù)時(shí),更推薦使用Numpy和Pandas等第三方庫(kù)。
10. 總結(jié)本篇首先按照一定的要求,定義了一個(gè)Vector2d類,重點(diǎn)是如果實(shí)現(xiàn)這個(gè)類的不同輸出形式;隨后,能從字節(jié)序列"反編譯"成我們需要的類,我們實(shí)現(xiàn)了一個(gè)備選構(gòu)造方法,順帶介紹了@classmethod和@staticmethod裝飾器;接著,我們通過重寫__format_方法,實(shí)現(xiàn)了自定義格式化輸出數(shù)據(jù);然后,通過使用@property裝飾器,定義"私有"屬性以及重寫__hash__方法等操作實(shí)現(xiàn)了這個(gè)類的可散列化。至此,關(guān)于Vector2d的內(nèi)容基本結(jié)束。最后,我們介紹了兩種常見類型的屬性(“私有”,“保護(hù)”),覆蓋類屬性以及如何通過__slots__節(jié)省內(nèi)存等問題。
本文實(shí)現(xiàn)了這么多特殊方法只是為展示如何編寫標(biāo)準(zhǔn)Python對(duì)象的API,如果你的應(yīng)用用不到這些內(nèi)容,大可不必為了滿足Python風(fēng)格而給自己增加負(fù)擔(dān)。畢竟,簡(jiǎn)潔勝于復(fù)雜。
迎大家關(guān)注我的微信公眾號(hào)"代碼港" & 個(gè)人網(wǎng)站 www.vpointer.net ~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/41863.html
摘要:函數(shù)內(nèi)省的內(nèi)容到此結(jié)束。函數(shù)式編程并不是一個(gè)函數(shù)式編程語言,但通過和等包的支持,也可以寫出函數(shù)式風(fēng)格的代碼。 《流暢的Python》筆記。本篇主要講述Python中函數(shù)的進(jìn)階內(nèi)容。包括函數(shù)和對(duì)象的關(guān)系,函數(shù)內(nèi)省,Python中的函數(shù)式編程。 1. 前言 本片首先介紹函數(shù)和對(duì)象的關(guān)系;隨后介紹函數(shù)和可調(diào)用對(duì)象的關(guān)系,以及函數(shù)內(nèi)省。函數(shù)內(nèi)省這部分會(huì)涉及很多與IDE和框架相關(guān)的東西,如果平時(shí)...
摘要:前言數(shù)據(jù)模型其實(shí)是對(duì)框架的描述,它規(guī)范了這門語言自身構(gòu)件模塊的接口,這些模塊包括但不限于序列迭代器函數(shù)類和上下文管理器。上述類實(shí)現(xiàn)了方法,它可用于需要布爾值的上下文中等。但多虧了它是特殊方法,我們也可以把用于自定義數(shù)據(jù)類型。 《流暢的Python》筆記。本篇是Python進(jìn)階篇的開始。本篇主要是對(duì)Python特殊方法的概述。 1. 前言 數(shù)據(jù)模型其實(shí)是對(duì)Python框架的描述,它規(guī)范了...
閱讀 1751·2023-04-25 22:42
閱讀 2202·2021-09-22 15:16
閱讀 3486·2021-08-30 09:44
閱讀 485·2019-08-29 16:44
閱讀 3304·2019-08-29 16:20
閱讀 2512·2019-08-29 16:12
閱讀 3387·2019-08-29 16:07
閱讀 666·2019-08-29 15:08