《流暢的Python》筆記。
本篇是「面向對象慣用方法」的第二篇。前一篇講的是內置對象的結構和行爲,本篇則是自定義對象。本篇繼續「Python學習之路20」,實現更多的特殊方法以讓自定義類的行爲跟真正的Python對象同樣。
本篇要討論的內容以下,重點放在了對象的各類輸出形式上:python
repr()
,bytes()
等);format()
函數和str.format()
方法使用的格式微語言;__slots__
節省內存;@classmethod
和@staticmethd
裝飾器;本篇將經過實現一個簡單的二維歐幾里得向量類型,來涵蓋上述內容。程序員
不過在開始以前,咱們須要補充幾個概念:數據庫
repr()
:以便於開發者理解的方式返回對象的字符串表示形式,它調用對象的__repr__
特殊方法;str()
:以便於用戶理解的方式返回對象的字符串表示形式,它調用對象的__str__
特殊方法;bytes()
:獲取對象的字節序列表示形式,它調用對象的__bytes__
特殊方法;format()
和str.format()
:格式化輸出對象的字符串表示形式,調用對象的__format__
特殊方法。咱們但願這個類具有以下行爲:數組
# 代碼1 >>> v1 = Vector2d(3, 4) >>> print(v1.x, v1.y) # Vector2d實例的份量可直接經過實例屬性訪問,無需調用讀值方法 3.0 4.0 >>> x, y = v1 # 實例可拆包成變量元組 >>> x, y (3.0, 4.0) >>> v1 # 咱們但願__repr__返回的結果相似於構造實例的源碼 Vector2d(3.0, 4.0) >>> v1_clone = eval(repr(v1)) # 只是爲了說明repr()返回的結果能用來生成實例 >>> v1 == v1_clone # Vector2d需支持 == 運算符 True >>> print(v1) # 咱們但願__str__方法以以下形式返回實例的字符串表示 (3.0, 4.0) >>> octets = bytes(v1) # 可以生成字節序列 >>> octets b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@' >>> abs(v1) # 可以求模 5.0 >>> bool(v1), bool(Vector2d(0, 0)) # 能進行布爾運算 (True, False)
Vector2d
的初始版本以下:bash
# 代碼2 from array import array import math class Vector2d: # 類屬性,在Vector2d實例和字節序列之間轉換時使用 typecode = "d" # 轉換成C語言中的double類型 def __init__(self, x, y): self.x = float(x) # 構造是就轉換成浮點數,儘早在構造階段就捕獲錯誤 self.y = float(y) def __iter__(self): # 將Vector2d實例變爲可迭代對象 return (i for i in (self.x, self.y)) # 這是生成器表達式! def __repr__(self): class_name = type(self).__name__ # 獲取類名,沒有采用硬編碼 # 因爲Vector2d實例是可迭代對象,因此*self會把x和y提供給format函數 return "{}({!r}, {!r})".format(class_name, *self) def __str__(self): return str(tuple(self)) # 由可迭代對象構造元組 def __bytes__(self): # ord()返回字符的Unicode碼位;array中的數組的元素是double類型 return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))) def __eq__(self, other): # 這樣實現有缺陷,Vector(3, 4) == [3, 4]也會返回True return tuple(self) == tuple(other) # 但這個缺陷會在後面章節修復 def __abs__(self): # 計算平方和的非負數根 return math.hypot(self.x, self.y) def __bool__(self): # 用到了上面的__abs__來計算模,若是模爲0,則是False,不然爲True return bool(abs(self))
第一版Vector2d
可將它的實例轉換成字節序列,但卻不能從字節序列構造Vector2d
實例,下面添加一個方法實現此功能:微信
# 代碼3 class Vector2d: -- snip -- @classmethod def frombytes(cls, octets): # 不用傳入self參數,但要經過cls傳入類自己 typecode = chr(octets[0]) # 從第一個字節中讀取typecode,chr()將Unicode碼位轉換成字符 # 使用傳入的octets字節序列構建一個memoryview,而後根據typecode轉換成所須要的數據類型 memv = memoryview(octets[1:]).cast(typecode) return cls(*memv) # 拆包轉換後的memoryview,而後構造一個Vector2d實例,並返回
代碼3
中用到了@classmethod
裝飾器,與它相伴的還有@staticmethod
裝飾器。閉包
從上述代碼能夠看出,classmethod
定義的是傳入類而不是傳入實例的方法,即傳入的第一個參數必須是類,而不是實例。classmethod
改變了調用方法的方式,可是,在實際調用這個方法時,咱們不須要手動傳入cls
這個參數,Python會自動傳入。(按照傳統,第一個參數通常命名爲cls
,固然你也能夠另起名)函數
staticmethod
也會改變方法的調用方式,但第一個參數不是特殊值,既不是cls
,也不是self
,就是用戶傳入的普通參數。如下是它們的用法對比:學習
# 代碼4 >>> class Demo: ... @classmethod ... def klassmeth(*args): ... return args # 返回傳入的所有參數 ... @staticmethod ... def statmeth(*args): ... return args # 返回傳入的所有參數 ... >>> Demo.klassmeth() (<class 'Demo'>,) # 無論如何調用Demo.klassmeth,它的第一個參數始終是Demo類本身 >>> Demo.klassmeth("spam") (<class 'Demo'>, 'spam') >>> Demo.statmeth() () # Demo.statmeth的行爲與普通函數相似 >>> Demo.statmeth("spam") ('spam',)
classmethod
頗有用,但staticmethod
通常都能找到很方便的替代方案,因此staticmethod
並非必須的。網站
內置的format()
函數和str.format()
方法把各個類型的格式化方式委託給相應的.__format__(format_spec)
方法。format_spec
是格式說明符,它是:
format(my_obj, format_spec)
的第二個參數;也是str.format()
方法的格式字符串,{}
裏替換字段中冒號後面的部分,例如:
# 代碼5 >>> brl = 1 / 2.43 >>> "1 BRL = {rate:0.2f} USD".format(rate=brl) # 此時 format_spec爲'0.2f'
其中,冒號後面的0.2f
是格式說明符,冒號前面的rate
是字段名稱,與格式說明符無關。格式說明符使用的表示法叫格式規範微語言(Format Specification Mini-Language)。格式規範微語言爲一些內置類型提供了專門的表示代碼,好比b
表示二進制的int
類型;同時它仍是可擴展的,各個類能夠自行決定如何解釋format_spec
參數,好比時間的轉換格式%H:%M:%S
,就可用於datetime
類型,但用於int
類型則可能報錯。
若是類沒有定義__format__
方法,則會返回__str__
的結果,好比咱們定義的Vector2d
類型就沒有定義__format__
方法,但依然能夠調用format()
函數:
# 代碼6 >>> v1 = Vector2d(3, 4) >>> format(v1) '(3.0, 4.0)'
但如今的Vector2d
在格式化顯示上還有缺陷,不能向format()
傳入格式說明符:
>>> format(v1, ".3f") Traceback (most recent call last): -- snip -- TypeError: non-empty format string passed to object.__format__
如今咱們來爲它定義__format__
方法。添加自定義的格式代碼,若是格式說明符以'p'
結尾,則以極座標的形式輸出向量,即<r, θ>
,'p'
以前的部分作正常處理;若是沒有'p'
,則按笛卡爾座標形式輸出。爲此,咱們還須要一個計算弧度的方法angle
:
# 代碼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)
如下是實際示例:
# 代碼8 >>> format(Vector2d(1, 1), "0.5fp") '<1.41421, 0.78540>' >>> format(Vector2d(1, 1), "0.5f") '(1.00000, 1.00000)'
關於可散列的概念能夠參考以前的文章《Python學習之路22》。
目前的Vector2d
是不可散列的,爲此咱們須要實現__hash__
特殊方法,而在此以前,咱們還要讓向量不可變,即self.x
和self.y
的值不能被修改。之因此要讓向量不可變,是由於咱們在計算向量的哈希值時須要用到self.x
和self.y
的哈希值,若是這兩個值可變,那向量的哈希值就能隨時變化,這將不是一個可散列的對象。
補充:
id()
的返回值。可是此處的Vector2d
倒是不可散列的,這是爲何?其實,若是咱們要讓自定義類變爲可散列的,正確的作法是同時實現__hash__
和__eq__
這兩個特殊方法。當這兩個方法都沒有重寫時,自定義類的哈希值就是id()
的返回值,此時自定義類可散列;當咱們只重寫了__hash__
方法時,自定義類也是可散列的,哈希值就是__hash__
的返回值;可是,若是隻重寫了__eq__
方法,而沒有重寫__hash__
方法,此時自定義類便不可散列。這裏再次給出可散列對象必須知足的三個條件:
hash()
函數,而且經過__hash__
方法所獲得的哈希值是不變的;__eq__
方法來檢測相等性;a == b
爲真,則hash(a) == hash(b)
也必須爲真。根據官方文檔,最好使用異或運算^
混合各份量的哈希值,下面是Vector2d
的改進:
# 代碼9 class Vector2d: -- snip -- def __init__(self, x, y): self.__x = float(x) self.__y = float(y) @property # 把方法變爲屬性調用,至關於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 --
文章至此說的都是一些特殊方法,若是想到獲得功能完善的對象,這些方法多是必備的,但若是你的應用用不到這些東西,則徹底沒有必要去實現這些方法,客戶並不關心你的對象是否符合Python風格。
Vector2d
暫時告一段落,如今來講一說其它比較雜的內容。
Python不像C++、Java那樣能夠用private
關鍵字來建立私有屬性,但在Python中,能夠以雙下劃線開頭來命名屬性以實現"私有"屬性,可是這種屬性會發生名稱改寫(name mangling):Python會在這樣的屬性前面加上一個下劃線和類名,而後再存入實例的__dict__
屬性中,以最新的Vector2d
爲例:
# 代碼10 >>> v1 = Vector2d(1, 2) >>> v1.__dict__ {'_Vector2d__x': 1.0, '_Vector2d__y': 2.0}
當屬性以雙下劃線開頭時,實際上是告訴別的程序員,不要直接訪問這個屬性,它是私有的。名稱改寫的目的是避免意外訪問,而不能防止故意訪問。只要你知道規則,這些屬性同樣能夠訪問。
還有以單下劃線開頭的屬性,這種屬性在Python的官方文檔的某個角落裏被稱爲了"受保護的"屬性,但Python不會對這種屬性作特殊處理,這只是一種約定俗成的規矩,告訴別的程序員不要試圖從外部訪問這些屬性。這種命名方式很常見,但其實不多有人把這種屬性叫作"受保護的"屬性。
仍是那句話,Python中全部的屬性都是公有的,Python沒有不能訪問的屬性!這些規則並不能阻止你有意訪問這些屬性,一切都看你遵不遵照上面這些"不成文"的規則了。
這裏首先須要區分兩個概念,類屬性與實例屬性:
Vector2d
中定義typecode
的方式來定義類屬性,即直接在class
中定義屬性,而不是在__init__
中;self
綁定,self
指向的是實例,而不是類。Python有個很獨特的特性:類屬性可用於爲實例屬性提供默認值。
Vector2d
中有個typecode
類屬性,注意到,咱們在__bytes__
方法中經過self.typecode
兩次用到了它,這裏明明是經過self
調用實例屬性,可Vector2d
的實例並無這個屬性。self.typecode
其實獲取的是Vector2d.typecode
類屬性的值,而至於怎麼從實例屬性跳到類屬性的,之後有機會單獨用一篇文章來說。
補充:證實實例沒有typecode
屬性
# 代碼11 >>> v = Vector2d(1, 2) >>> v.__dict__ {'_Vector2d__x': 1.0, '_Vector2d__y': 2.0} # 實例中並無typecode屬性
若是爲不存在的實例屬性賦值,則會新建該實例屬性。假如咱們爲typecode
實例屬性賦值,同名類屬性不會受到影響,但會被實例屬性給覆蓋掉(相似於以前在函數閉包中講的局部變量和全局變量的區別)。藉助這一特性,能夠爲各個實例的typecode
屬性定製不一樣的值,好比在生成字節序列時,將實例轉換成4字節的單精度浮點數:
# 代碼12 >>> v1 = Vector2d(1.1, 2.2) >>> dumpd = bytes(v1) # 按雙精度轉換 >>> dumpd b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@' >>> len(dumpd) 17 >>> v1.typecode = "f" >>> dumpf = bytes(v1) # 按單精度轉換 >>> dumpf b'f\xcd\xcc\x8c?\xcd\xcc\x0c@' # 明白爲何要在字節序列前加上typecode的值了嗎?爲了支持不一樣格式。 >>> len(dumpf) 9 >>> Vector2d.typecode 'd'
若是想要修改類屬性的值,必須直接在類上修改,不能經過實例修改。若是想修改全部實例的typecode
屬性的默認值,能夠這麼作:
# 代碼13 Vector2d.typecode = "f"
然而有種方式更符合Python風格,並且效果持久,也更有針對性。經過繼承的方式修改類屬性,生成專門的子類。Django基於類的視圖就大量使用了這個技術:
# 代碼14 >>> class ShortVector2d(Vector2d): ... typecode = "f" # 只修改這一處 ... >>> sv = ShortVector2d(1/11, 1/27) >>> sv ShortVector2d(0.09090909090909091, 0.037037037037037035) # 沒有硬編碼class_name的緣由 >>> len(bytes(sv)) 9
默認狀況下,Python在各個實例的__dict__
屬性中以映射類型存儲實例屬性。正如《Python學習之路22》中所述,爲了使用底層的散列表提高訪問速度,字典會消耗大量內存。若是要處理數百萬個屬性很少的實例,其實能夠經過__slots__
類屬性來節省大量內存。作法是讓解釋器用相似元組的結構存儲實例屬性,而不是字典。
具體用法是,在類中建立這個__slots__
類屬性,並把它的值設爲一個可迭代對象,其中的元素是其他實例屬性的字符串表示。好比咱們將以前定義的Vector2d
改成__slots__
版本:
# 代碼15 class Vector2d: __slots__ = ("__x", "__y") typecode = "d" # 其他保持不變 -- snip --
試驗代表,建立一千萬個以前版本的Vector2d
實例,內存用量高達1.5GB,而__slots__
版本的Vector2d
的內存用量不到700MB,而且速度也比以前的版本快。
但__slots__
也有一些須要注意的點:
__slots__
以後,實例不能再有__slots__
中所列名稱以外的屬性,即,不能動態添加屬性;若是要使其能動態添加屬性,必須在其中加入'__dict__'
,但這麼作又違背了初衷;__slots__
屬性,解釋器會忽略掉父類的__slots__
屬性;__weakref__
屬性,但若是定義了__slots__
屬性,並且還要自定義類支持弱引用,則須要把'__weakref__'
加入到__slots__
中。總之,不要濫用__slots__
屬性,也不要用它來限制用戶動態添加屬性(除非有意爲之)。__slots__
在處理列表數據時最有用,例如模式固定的數據庫記錄,以及特大型數據集。然而,當遇到這類數據時,更推薦使用Numpy和Pandas等第三方庫。
本篇首先按照必定的要求,定義了一個Vector2d
類,重點是若是實現這個類的不一樣輸出形式;隨後,能從字節序列"反編譯"成咱們須要的類,咱們實現了一個備選構造方法,順帶介紹了@classmethod
和@staticmethod
裝飾器;接着,咱們經過重寫__format_
方法,實現了自定義格式化輸出數據;而後,經過使用@property
裝飾器,定義"私有"屬性以及重寫__hash__
方法等操做實現了這個類的可散列化。至此,關於Vector2d
的內容基本結束。最後,咱們介紹了兩種常見類型的屬性(「私有」,「保護」),覆蓋類屬性以及如何經過__slots__
節省內存等問題。
本文實現了這麼多特殊方法只是爲展現如何編寫標準Python對象的API,若是你的應用用不到這些內容,大可沒必要爲了知足Python風格而給本身增長負擔。畢竟,簡潔勝於複雜。
迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~