Python學習之路20-數據模型

本篇是Python進階篇的開始。本篇主要是對Python特殊方法的概述。

1. 前言

數據模型其實是對Python框架的描述,它規範了這門語言自身構件模塊的接口,這些模塊包括但不限於序列、迭代器、函數、類和上下文管理器。不管在哪種框架下寫程序,都會花費大量時間去實現那些會被框架本身調用的方法,Python也不例外。Python解釋器碰到特殊句法時,會使用特殊方法去激活一些基本的對象操作,這些特殊方法的名字以兩個下劃線開頭,以兩個下劃線結尾(所以特殊方法也叫雙下方法 dunder method),這些特殊方法名能讓自己編寫的對象實現和支持以下的語言構架,並與之交互:

迭代、集合類、屬性訪問、運算符重載、函數和方法的調用、對象的創建和銷燬、字符串表示形式和格式化、管理上下文(即with塊)。

下面通過一些例子來介紹常用的特殊方法。

2. Python風格紙牌

首先介紹兩個特殊方法 __getitem__ 和 __len__ 這兩個特殊方法。以下代碼創建了一個紙牌類:

Python學習之路20-數據模型

namedtuple,即命名元組,類似於C/C++中的struct,定義如下:

collections.namedtuple(typename, field_names, verbose=False, rename=False)

第一個參數是元組名;第二個是該元組中含的屬性名;第三個參數表示在構建該命名元組之前先打印出該命名元組的結構,如果在控制檯輸入第3行代碼,並置verbose為True的話,會輸出該命名元組的內部結構,實際上它是一個繼承自tuple的類,由於輸出過長,請大家自行實驗;如果該命名元組的元素名中有Python關鍵字,則需要置第四個參數為True,這些與關鍵字重名的元素名會被特殊處理。

用命名元組創建一個不帶方法的對象十分簡單:

Python學習之路20-數據模型

由於FrenchDeck實現了 __getitem__ 方法,所以可以像操作List或Tuple一樣操作FrenchDeck,比如隨機訪問,切片:

Python學習之路20-數據模型

由於實現了該方法,FrenchDeck還是個可迭代對象,即可以用for循環對其訪問(也可以反向訪問reversed):

Python學習之路20-數據模型

迭代通常是隱式的,譬如說一個集合類型沒有實現__contains__方法,那麼in運算符就會按順序做一次迭代搜索(調用__getitem__),於是in運算符可以用在FrenchDeck上:

Python學習之路20-數據模型

如果對上述deck變量調用sorted函數,Python將按ASCII碼進行排序,但這並不是撲克牌的正確排序,所以下面我們自定義排序方法:

Python學習之路20-數據模型

此時輸出的結果就是先按點數排序,再按花色排序。

3. 如何使用特殊方法

需要明確一點,特殊方法的存在是為了給Python解釋器調用到,作為程序員並不需要調用他們,也即是說,沒有my_object.__len__()這種寫法,而應該是len(my_object)。說到__len__方法,如果是Python內置類型,CPython會抄個近路,該方法實際上會直接返回PyVarObject裡的ob_size屬性,而PyVarObject是表示內存中長度可變的內痔對象的C語言結構體。

很多時候特殊方法的調用是隱式的,比如for i in x: 這個語句,背後其實用的是iter(x),而這個函數的背後則是x.__iter__()方法,當然前提是這個方法在x中被實現(如果沒被實現則會調用__getitem__方法)。

直接調用這個值比調用一個方法快很多。直接調用特殊方法的頻率應該遠遠低於你去實現它們的次數。

通過內置的函數(例如len,iter,str等)來使用特殊方法是最好的選擇。這些內置函數不僅會調用特殊方法,通常還提供額外的好處,而且對於內置的類來說,它們的速度更快。

還有一點值得注意:不要想當然地隨意添加特殊方法,比如__foo__之類的,因為雖然現在這個名字沒有被Python內部使用,以後就不一定了。

3.1 自定義向量Vector

使用5個特殊方法實現Vector的字符串輸出,取絕對值(如果是複數則是取模),返回布爾值,加法和數乘等運算:

Python學習之路20-數據模型

Python有一個內置函數叫做repr。該函數通過特殊方法__repr__來得到一個對象的字符串表示形式,如果沒有該特殊方法,當我們在控制檯打印一個向量對象時,得到的字符串可能是

Python學習之路20-數據模型

__repr__ 與 __str__的區別與聯繫:前者方便我們調試和記錄日誌,後者則是給終端用戶看的。後者是在str()函數被使用,或者是在print函數打印一個對象的時候才被調用,它返回的字符串對終端用戶友好。如果只想實現這兩個特殊方法中的一個,__repr__ 是更好的選擇,因為如果一個對象沒有 __str__ 函數,Python又需要調用它時,解釋器會用 __repr__ 代替。

上述Vector類實現了 __bool__ 方法,它可用於需要布爾值的上下文中(if, while, and, or, not等)。默認情況下,我們自己定義的類的實例總被認為是True,除非重寫了這個類的 __bool__ 或 __len__ 方法。bool(x)的背後是調用 x.__bool__();如果不存在 __bool__ 方法,那麼bool(x)會嘗試調用 x.__len__(),如果該方法返回0,則bool返回False,否則返回True。

3.2 為什麼len不是普通方法

“實用勝於純粹”(Python之禪裡的一句話)。len之所以不是一個普通方法,是為了讓Python自帶的數據結構可以走後門,abs也是同理。但多虧了它是特殊方法,我們也可以把len用於自定義數據類型。這種處理方式在保持內置類型的效率和保證語言的一致性之間找到了一個平衡點,也印證了“Python之禪”中的另一句話:“不能讓特例特殊到考試破壞既定規則”。

4. 總結

通過實現特殊方法,自定義數據類型可以表現得跟內置類型一樣,從而讓我們寫出更具Python風格(Pythonic)的代碼。後面的內容將圍繞更多的特殊方法展開。


分享到:


相關文章: