非常全的通俗易懂 Python 魔法方法指南(下)
- 2020 年 2 月 26 日
- 筆記
點擊上方「鹹魚學Python」,選擇「加為星標」
第一時間關注Python技術乾貨!

作者:Rafe Kettler
翻譯:hit9
來源:https://pyzh.readthedocs.io/en/latest/python-magic-methods-guide.html
06. 反射
你可以通過定義魔法方法來控制用於反射的內建函數 isinstance 和 issubclass 的行為。下面是對應的魔法方法:
__instancecheck__
(self, instance) 檢查一個實例是否是你定義的類的一個實例(例如 isinstance(instance, class) )。__subclasscheck__
(self, subclass) 檢查一個類是否是你定義的類的子類(例如 issubclass(subclass, class) )。
這幾個魔法方法的適用範圍看起來有些窄,事實也正是如此。我不會在反射魔法方法上花費太多時間,因為相比其他魔法方法它們顯得不是很重要。但是它們展示了在Python中進行面向對象編程(或者總體上使用Python進行編程)時很重要的一點:不管做什麼事情,都會有一個簡單方法,不管它常用不常用。這些魔法方法可能看起來沒那麼有用,但是當你真正需要用到它們的時候,你會感到很幸運,因為它們還在那兒(也因為你閱讀了這本指南!)
07. 抽象基類
請參考 http://docs.python.org/2/library/abc.html
08. 可調用的對象
你可能已經知道了,在Python中,函數是一等的對象。這意味著它們可以像其他任何對象一樣被傳遞到函數和方法中,這是一個十分強大的特性。
Python中一個特殊的魔法方法允許你自己類的對象表現得像是函數,然後你就可以「調用」它們,把它們傳遞到使用函數做參數的函數中,等等等等。這是另一個強大而且方便的特性,讓使用Python編程變得更加幸福。
__call__
(self, [args…]) 允許類的一個實例像函數那樣被調用。本質上這代表了 x() 和 x.__call__
() 是相同的。注意__call__
可以有多個參數,這代表你可以像定義其他任何函數一樣,定義__call__
,喜歡用多少參數就用多少。
__call__
在某些需要經常改變狀態的類的實例中顯得特別有用。「調用」這個實例來改變它的狀態,是一種更加符合直覺,也更加優雅的方法。一個表示平面上實體的類是一個不錯的例子:
class Entity: '''表示一個實體的類,調用它的實例 可以更新實體的位置''' def __init__(self, size, x, y): self.x, self.y = x, y self.size = size def __call__(self, x, y): '''改變實體的位置''' self.x, self.y = x, y
09. 上下文管理器
在Python 2.5中引入了一個全新的關鍵詞,隨之而來的是一種新的程式碼復用方法—— with 聲明。上下文管理的概念在Python中並不是全新引入的(之前它作為標準庫的一部分實現),直到PEP 343被接受,它才成為一種一級的語言結構。可能你已經見過這種寫法了:
with open('foo.txt') as bar: # 使用bar進行某些操作
當對象使用 with 聲明創建時,上下文管理器允許類做一些設置和清理工作。上下文管理器的行為由下面兩個魔法方法所定義:
__enter__
(self) 定義使用 with 聲明創建的語句塊最開始上下文管理器應該做些什麼。注意__enter__
的返回值會賦給 with 聲明的目標,也就是 as 之後的東西。__exit__
(self, exception_type, exception_value, traceback) 定義當 with 聲明語句塊執行完畢(或終止)時上下文管理器的行為。它可以用來處理異常,進行清理,或者做其他應該在語句塊結束之後立刻執行的工作。如果語句塊順利執行, exception_type , exception_value 和 traceback 會是 None 。否則,你可以選擇處理這個異常或者讓用戶來處理。如果你想處理異常,確保__exit__
在完成工作之後返回 True 。如果你不想處理異常,那就讓它發生吧。
對一些具有良好定義的且通用的設置和清理行為的類,__enter__
和 __exit__
會顯得特別有用。你也可以使用這幾個方法來創建通用的上下文管理器,用來包裝其他對象。下面是一個例子:
class Closer: '''一個上下文管理器,可以在with語句中 使用close()自動關閉對象''' def __init__(self, obj): self.obj = obj def __enter__(self, obj): return self.obj # 綁定到目標 def __exit__(self, exception_type, exception_value, traceback): try: self.obj.close() except AttributeError: # obj不是可關閉的 print 'Not closable.' return True # 成功地處理了異常
這是一個 Closer 在實際使用中的例子,使用一個FTP連接來演示(一個可關閉的socket):
>>> from magicmethods import Closer >>> from ftplib import FTP >>> with Closer(FTP('ftp.somesite.com')) as conn: ... conn.dir() ... # 為了簡單,省略了某些輸出 >>> conn.dir() # 很長的 AttributeError 資訊,不能使用一個已關閉的連接 >>> with Closer(int()) as i: ... i += ... Not closable. >>> i
看到我們的包裝器是如何同時優雅地處理正確和不正確的調用了嗎?這就是上下文管理器和魔法方法的力量。Python標準庫包含一個 contextlib 模組,裡面有一個上下文管理器 contextlib.closing() 基本上和我們的包裝器完成的是同樣的事情(但是沒有包含任何當對象沒有close()方法時的處理)。
10. 創建描述符對象
描述符是一個類,當使用取值,賦值和刪除 時它可以改變其他對象。描述符不是用來單獨使用的,它們需要被一個擁有者類所包含。描述符可以用來創建面向對象資料庫,以及創建某些屬性之間互相依賴的類。描述符在表現具有不同單位的屬性,或者需要計算的屬性時顯得特別有用(例如表現一個坐標系中的點的類,其中的距離原點的距離這種屬性)。
要想成為一個描述符,一個類必須具有實現 __get__
, __set__
和 __delete__
三個方法中至少一個。
讓我們一起來看一看這些魔法方法:
__get__
(self, instance, owner) 定義當試圖取出描述符的值時的行為。instance 是擁有者類的實例, owner 是擁有者類本身。__set__
(self, instance, owner) 定義當描述符的值改變時的行為。instance 是擁有者類的實例, value 是要賦給描述符的值。__delete__
(self, instance, owner) 定義當描述符的值被刪除時的行為。instance 是擁有者類的實例
現在,來看一個描述符的有效應用:單位轉換:
class Meter(object): '''米的描述符。''' def __init__(self, value=0.0): self.value = float(value) def __get__(self, instance, owner): return self.value def __set__(self, instance, owner): self.value = float(value) class Foot(object): '''英尺的描述符。''' def __get__(self, instance, owner): return instance.meter * 3.2808 def __set__(self, instance, value): instance.meter = float(value) / 3.2808 class Distance(object): '''用於描述距離的類,包含英尺和米兩個描述符。''' meter = Meter() foot = Foot()
11. 拷貝
有些時候,特別是處理可變對象時,你可能想拷貝一個對象,改變這個對象而不影響原有的對象。這時就需要用到Python的 copy 模組了。然而(幸運的是),Python模組並不具有感知能力, 因此我們不用擔心某天基於Linux的機器人崛起。但是我們的確需要告訴Python如何有效率地拷貝對象。
__copy__
(self) 定義對類的實例使用 copy.copy() 時的行為。copy.copy() 返回一個對象的淺拷貝,這意味著拷貝出的實例是全新的,然而裡面的數據全都是引用的。也就是說,對象本身是拷貝的,但是它的數據還是引用的(所以淺拷貝中的數據更改會影響原對象)。__deepcopy__
(self, memodict=) 定義對類的實例使用 copy.deepcopy() 時的行為。copy.deepcopy() 返回一個對象的深拷貝,這個對象和它的數據全都被拷貝了一份。memodict 是一個先前拷貝對象的快取,它優化了拷貝過程,而且可以防止拷貝遞歸數據結構時產生無限遞歸。當你想深拷貝一個單獨的屬性時,在那個屬性上調用 copy.deepcopy() ,使用 memodict 作為第一個參數。
這些魔法方法有什麼用武之地呢?像往常一樣,當你需要比默認行為更加精確的控制時。例如,如果你想拷貝一個對象,其中存儲了一個字典作為快取(可能會很大),拷貝快取可能是沒有意義的。如果這個快取可以在記憶體中被不同實例共享,那麼它就應該被共享。
12. Pickling
如果你和其他的Python愛好者共事過,很可能你已經聽說過Pickling了。Pickling是Python數據結構的序列化過程,當你想存儲一個對象稍後再取出讀取時,Pickling會顯得十分有用。然而它同樣也是擔憂和混淆的主要來源。
Pickling是如此的重要,以至於它不僅僅有自己的模組( pickle ),還有自己的協議和魔法方法。首先,我們先來簡要的介紹一下如何pickle已存在的對象類型(如果你已經知道了,大可跳過這部分內容)。
12.1 小試牛刀
我們一起來pickle吧。假設你有一個字典,你想存儲它,稍後再取出來。你可以把它的內容寫入一個文件,小心翼翼地確保使用了正確地格式,要把它讀取出來,你可以使用 exec() 或處理文件輸入。但是這種方法並不可靠:如果你使用純文本來存儲重要數據,數據很容易以多種方式被破壞或者修改,導致你的程式崩潰,更糟糕的情況下,還可能在你的電腦上運行惡意程式碼。因此,我們要pickle它:
import pickle data = {'foo': [,,], 'bar': ('Hello', 'world!'), 'baz': True} jar = open('data.pkl', 'wb') pickle.dump(data, jar) # 將pickle後的數據寫入jar文件 jar.close()
過了幾個小時,我們想把它取出來,我們只需要反pickle它:
import pickle pkl_file = open('data.pkl', 'rb') # 與pickle後的數據連接 data = pickle.load(pkl_file) # 把它載入進一個變數 print data pkl_file.close()
將會發生什麼?正如你期待的,它就是我們之前的 data 。
現在,還需要謹慎地說一句:pickle並不完美。Pickle文件很容易因為事故或被故意的破壞掉。Pickling或許比純文本文件安全一些,但是依然有可能被用來運行惡意程式碼。而且它還不支援跨Python版本,所以不要指望分發pickle對象之後所有人都能正確地讀取。然而不管怎麼樣,它依然是一個強有力的工具,可以用於快取和其他類型的持久化工作。
12.2 Pickle你的對象
Pickle不僅僅可以用於內建類型,任何遵守pickle協議的類都可以被pickle。Pickle協議有四個可選方法,可以讓類自定義它們的行為(這和C語言擴展略有不同,那不在我們的討論範圍之內)。
__getinitargs__
(self) 如果你想讓你的類在反pickle時調用__init__
,你可以定義__getinitargs__
(self) ,它會返回一個參數元組,這個元組會傳遞給__init__
。注意,這個方法只能用於舊式類。__getnewargs__
(self) 對新式類來說,你可以通過這個方法改變類在反pickle時傳遞給__new__
的參數。這個方法應該返回一個參數元組。__getstate__
(self) 你可以自定義對象被pickle時被存儲的狀態,而不使用對象的__dict__
屬性。這個狀態在對象被反pickle時會被__setstate__
使用。__setstate__
(self) 當一個對象被反pickle時,如果定義了__setstate__
,對象的狀態會傳遞給這個魔法方法,而不是直接應用到對象的__dict__
屬性。這個魔法方法和__getstate__
相互依存:當這兩個方法都被定義時,你可以在Pickle時使用任何方法保存對象的任何狀態。__reduce__
(self) 當定義擴展類型時(也就是使用Python的C語言API實現的類型),如果你想pickle它們,你必須告訴Python如何pickle它們。reduce 被定義之後,當對象被Pickle時就會被調用。它要麼返回一個代表全局名稱的字元串,Pyhton會查找它並pickle,要麼返回一個元組。這個元組包含2到5個元素,其中包括:一個可調用的對象,用於重建對象時調用;一個參數元素,供那個可調用對象使用;被傳遞給__setstate__
的狀態(可選);一個產生被pickle的列表元素的迭代器(可選);一個產生被pickle的字典元素的迭代器(可選);__reduce_ex__
(self)__reduce_ex__
的存在是為了兼容性。如果它被定義,在pickle時__reduce_ex__
會代替__reduce__
被調用。__reduce__
也可以被定義,用於不支援__reduce_ex__
的舊版pickle的API調用。
12.3 一個例子
我們的例子是 Slate ,它會記住它的值曾經是什麼,以及那些值是什麼時候賦給它的。然而 每次被pickle時它都會變成空白,因為當前的值不會被存儲:
import time class Slate: '''存儲一個字元串和一個變更日誌的類 每次被pickle都會忘記它當前的值''' def __init__(self, value): self.value = value self.last_change = time.asctime() self.history = {} def change(self, new_value): # 改變當前值,將上一個值記錄到歷史 self.history[self.last_change] = self.value self.value = new_value) self.last_change = time.asctime() def print_change(self): print 'Changelog for Slate object:' for k,v in self.history.items(): print '%st %s' % (k,v) def __getstate__(self): # 故意不返回self.value或self.last_change # 我們想在反pickle時得到一個空白的slate return self.history def __setstate__(self): # 使self.history = slate,last_change # 和value為未定義 self.history = state self.value, self.last_change = None, None
總結在最後
這本指南的目標是使所有閱讀它的人都能有所收穫,無論他們有沒有使用Python或者進行面向對象編程的經驗。如果你剛剛開始學習Python,你會得到寶貴的基礎知識,了解如何寫出具有豐富特性的,優雅而且易用的類。如果你是中級的Python程式設計師,你或許能掌握一些新的概念和技巧,以及一些可以減少程式碼行數的好辦法。如果你是專家級別的Python愛好者,你又重新複習了一遍某些可能已經忘掉的知識,也可能順便了解了一些新技巧。無論你的水平怎樣,我希望這趟遨遊Python特殊方法的旅行,真的對你產生了魔法般的效果(實在忍不住不說最後這個雙關)。
Love & Share

[ 完 ]