非常全的通俗易懂 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

[ 完 ]