Python 的 Magic Methods 指南

發表於2016-01-11

介紹

本指南是數月部落格的總結。主題是魔術方法。

什麼是魔術方法呢?它們是物件導向Python語言中的一切。它們是你可以自定義並新增“魔法”到類中的特殊方法。它們被雙下劃線環繞(比如__init__或__lt__)。它們的文件也不像它所需要的那麼齊備。Python的所有魔術方法都在Python文件的同一區域,但它們的使用分散,組織鬆散。而且文件的這部分割槽域中幾乎沒有一個示例(這很有可能是設計好的,因為在語法參考裡它們都很詳盡,但伴隨的是枯燥的語法描述等等)。

因此,為了解決Python文件中我認為的缺陷,我想提供一些更簡單直白的表述——示例驅動型的Python魔術方法文件。我從每週的部落格開始,現在我已經完成了,並把它們合到了一起。

我希望你能喜歡它。把它作為一個教程、複習或參考使用;它希望能成為一個Python魔術方法使用者友好的指導。

構造與初始化

我們每個知道的最基本的“魔法”方法是__init__。一種讓我們在初始化一個類時定義一些行為。然而當我執行 x = SomeClass(), __init__ 不是第一個被執行的。事實上,第一被執行的的方法是__new__,它會建立一個例項,然後在構造器建立時傳遞一些引數。在一個object的生命週期的另一端的方法是__del__。讓我們仔細看看這3個“魔法”方法:

  • __new__(cls, […)
  • __new__ 是一個類的初始化過程中第一個被執行的方法。它建立了類,然後把一些引數傳遞給__init__。__new__ 很少被使用,特別是當我們用一些不可變型別的子類時(像tuple ,string),我不想關心__new__的太多的細節,因為那是沒有用的。但它有它存在的意義。更多詳細的請看 in the Python docs.
  • __init__(self, […)
  • 類的構造器,當初始構造方法被執行(例如,我們執行 x = SomeClass(10,’foo’)),__init__ 就會獲得 10 和 ‘foo’ 作為引數。__init__ 在python類的定義中經常被使用
  • __del__(self)
  • 若果 __new__ 和 __init__ 形成一個類的建構函式,__del__ 是就是解構函式。它不實現語句 del x 的行為(這樣程式碼就不會轉換為 x.__del__())。它定義了一個被垃圾回收的行為。它在類消除的時後需要做一些額外的行為時是非常有用的,就像 sockets 和 file 類。注意,當編譯器還在執行,如果類還存活著,這裡不能確保__del__一定會被執行。所以__del__ 不能替代一些良好的程式設計習慣(比如連線用完了將其關掉),事實上__del__很少被使用,因為它的呼叫是非常不穩定的;請謹慎使用!

把他們合起來後,這裡就是一個 __init__ 和 __del__ 使用的例子:

 

定義自己的類中的操作

我們使用Python的“魔法”方法最大得優勢之一是它提供了一種簡單的方法去定義類的行為,比如 built-in 型別。這就意味著你可以避免醜陋的,違反直覺的,非標準化的基本操作方法。在一些語言中,他們通常這樣寫:

當讓Python中也可以這麼做,但是這增加了混亂和不必要的冗餘。不同的類庫中的相同的方法可能會用不同名字,使得使用者做了太多不必要的操作。相比之下“魔法”方法是強大的,我們可以使用它定義一個方法代替上面的例子(__eq__ , 在這個例子中):

這是“魔法”方法強大用途的一部分。他們絕大部分讓我們定義操作的意義,以至於我們可以使用他們在我們自己的類中就像使用built in 型別。

魔法比較方法

python擁有大量用於實現物件與物件之間比較的魔法方法,這些物件使用運算子進行直觀比較而不是難看的方法呼叫。同時它也提供了一種方法去過載python預設的物件比較行為(比較引用)。這裡有一個這些方法和它們做了什麼事情的列表:

  • __cmp__(self, other)
  • __cmp__ 是比較方法裡面最基本的的魔法方法。實際上它實現了所有的比較運算子(如<, ==, !=)的行為,但也許不是你想要的行為(例如,一個例項是否和另一個例項相等應該由某個條件來決定,一個例項是否大於另一個例項應該由其他的條件來決定)。當self < other時__cmp__應該返回一個負整數,當self == other時返回0,self > other時返回正整數。通常來說最好是定義每個你需要的比較方法而不是一次性定義所有的比較方法,但是__cmp__是一個消除重複性的良途,並且當你有很多比較方法需要用相類似的條件去實現的時候這能讓程式碼變得清晰。
  • __eq__(self, other)
  • 定義相等符號的行為,==
  • __ne__(self,other)
  • 定義不等符號的行為,!=
  • __lt__(self,other)
  • 定義小於符號的行為,<
  • __gt__(self,other)
  • 定義大於符號的行為,>
  • __le__(self,other)
  • 定義小於等於符號的行為,<=
  • __ge__(self,other)
  • 定義大於等於符號的行為,>=

例如,假設一個類是一個單詞模型。我們可能要按字典比較單詞(按字母),這是比較字串預設行為,但我們也可能需要基於其他一些標準來做比較,比如按長度、或位元組數量等。在下面的例子中,我們將比較長度。下面是實現:

現在,我們可以建立兩個單詞(通過使用Word(‘foo’)和Word(‘bar’))然後依據長度比較它們。但要注意,我們沒有定義__eq__和__ne__,因為這樣會導致其他一些奇怪的行為(尤其是Word(‘foo’)==Word(‘bar’)會判定為true),它不是基於長度相等意義上的測量,所以我們迴歸到字元平等意義上的實現。

現在需要留心啦——為達到預期的比較效果你不需要為每個比較定義魔術方法。如果你只定義__eq__以及其他的(比如__gt__,__lt__等),標準庫已經在functools模組裡為我們提供了一個類修飾器,它可以定義所有的富特性比較方法。這個特性只有在Python 2.7中才是可用的,但如果你碰巧的話這可以節省大量的時間和精力。你可以通過將@total_ordering放置在類定義前面來使用它。

數值魔術方法

就如同你可以通過定義比較操作來比較你自己的類例項一樣,你也可以自己定義數學運算子號的行為。好吧,先繫緊你的褲腰帶,深呼吸……,這些操作可多著呢。由於文章組織需要,我把這些數學“魔術方法”分為5類:單目運算操作,一般數學運算操作,滿足交換律的數學運算(後面會有更多介紹),引數賦值操作和型別轉換操作:

單目運算子操作與函式:

單目運算子或單目運算函式只有一個運算元: 比如取負(-2),絕對值操作等。

  • __pos__(self)
  • 實現一個取正數的操作(比如 +some_object ,python呼叫__pos__函式)
  • __neg__(self)
  • 實現一個取負數的操作(比如 -some_object )
  • __abs__(self)
  • 實現一個內建的abs()函式的行為
  • __invert__(self)
  • 實現一個取反操作符(~操作符)的行為。想要了解這個操作的解釋,參考the Wikipedia article on bitwise operations.
  • __round__(self, n)
  • 實現一個內建的round()函式的行為。 n 是待取整的十進位制數.
  • __floor__(self)
  • 實現math.floor()的函式行為,比如, 把數字下取整到最近的整數.
  • __ceil__(self)
  • 實現math.ceil()的函式行為,比如, 把數字上取整到最近的整數.
  • __trunc__(self)
  • 實現math.trunc()的函式行為,比如, 把數字截斷而得到整數.
  • 一般算數運算

    好吧,現在我們開始介紹雙目運算操作或函式,比如 +, -, * 等等. 這些很容易自解釋.

    • __add__(self, other)
    • 實現一個加法.
    • __sub__(self, other)
    • 實現一個減法.
    • __mul__(self, other)
    • 實現一個乘法.
    • __floordiv__(self, other)
    • 實現一個“//”操作符產生的整除操作()
    • __div__(self, other)
    • 實現一個“/”操作符代表的除法操作.
    • __truediv__(self, other)
    • 實現真實除法,注意,只有當你from __future__ import division時才會有效
    • __mod__(self, other)實現一個“%”操作符代表的取模操作.
    • __divmod__(self, other)
    • 實現一個內建函式divmod()
    • __pow__
    • 實現一個指數操作(“**”操作符)的行為
    • __lshift__(self, other)
    • 實現一個位左移操作(<<)的功能
    • __rshift__(self, other)
    • 實現一個位右移操作(>>)的功能.
    • __and__(self, other)
    • 實現一個按位進行與操作(&)的行為.
    • __or__(self, other)實現一個按位進行或操作(|)的行為.
    • __xor__(self, other)
    • 實現一個異或操作(^)的行為

反射算術運算子

你相信我說我能用一位來表示反射運算嗎?可能有人會認為表示一個反射運算是大的嚇人的“外國概念”,反射實際上它是非常簡單的。看下面的例子:

這是一個正常的加法。除了可以交換運算元以外,反射運算和加法是一樣的:

除了執行那種 other對像作為第一個運算元,而它自身作為第二個運算元的運算以外,所有的魔法方法做的事情與正常運算表示的意義是等價的。在大部分情況下反射運算結果和它正常的運算是等價的,所以你可以不定義__radd__,而是呼叫__add__等等。注意,對像(本例中的other)在運算子左邊的時候,必須保證該對像沒有定義(或者返回NotImplemented的)它的非反射運算子。例如,在這個例子中,some_object.__radd__  只有在 other沒有定義__add__的時候才會被呼叫。

  • __radd__(self, other)
  • 反射加法
  • __rsub__(self, other)
  • 反射減法的
  • __rmul__(self, other)
  • 反射除法
  • __rfloordiv__(self, other)
  • 反射地板除,使用//運算子的
  • __rdiv__(self, other)
  • 反射除法,使用/運算子的.
  • __rtruediv__(self, other)
  • 反射真除.注意只有from __future__ import division 的時候它才有效
  • __rmod__(self, other)
  • 反射取模運算,使用%運算子.
  • __rdivmod__(self, other)
  • 長除法,使用divmod()內建函式,當divmod(other,self)時被呼叫.
  • __rpow__
  • 反射乘方,使用**運算子的
  • __rlshift__(self, other)
  • 反射左移,使用<<操作符.
  • __rrshift__(self, other)
  • 反射右移,使用>>操作符.
  • __rand__(self, other)
  • 反射位與,使用&操作符.
  • __ror__(self, other)
  • 反射位或,使用|操作符.
  • __rxor__(self, other)
  • 反射異或,使用^操作符.

增量運算

Python 還有很多種魔法方法,允許一些習慣行為被定義成增量運算。你很可能已經熟悉了增量運算,增量運算是算術運算和賦值運算的結合。如果你還不知道我在說什麼,就看一下下面的例子:

每一個方法的返回值都會被賦給左邊的變數。(比如,對於a += b, __iadd__ 可能會返回a + b, a + b會賦給變數a。) 下面是清單:

  • __iadd__(self, other)
  • 加法賦值
  • __isub__(self, other)
  • 減法賦值.
  • __imul__(self, other)
  • 乘法賦值
  • __ifloordiv__(self, other)
  • 整除賦值,地板除,相當於 //= 運算子.
  • __idiv__(self, other)
  • 除法賦值,相當於 /= 運算子.
  • __itruediv__(self, other)
  • 真除賦值,注意只有你 whenfrom __future__ import divisionis,才有效.
  • __imod_(self, other)
  • 模賦值,相當於 %= 運算子.
  • __ipow__
  • 乘方賦值,相當於 **= 運算子.
  • __ilshift__(self, other)
  • 左移賦值,相當於 <<= 運算子.
  • __irshift__(self, other)
  • 左移賦值,相當於 >>= 運算子.
  • __iand__(self, other)
  • 與賦值,相當於 &= 運算子.
  • __ior__(self, other)
  • 或賦值,相當於 |= 運算子.
  • __ixor__(self, other)
  • 異或運算子,相當於 ^= 運算子.

型別轉換魔法

Python 同樣有一系列的魔法方法旨在實現內建型別的轉換,比如float() 函式。它們是:

  • __int__(self)
  • 轉換成整型.
  • __long__(self)
  • 轉換成長整型.
  • __float__(self)
  • 轉換成浮點型.
  • __complex__(self)
  • 轉換成 複數型.
  • __oct__(self)
  • 轉換成八進位制.
  • __hex__(self)
  • 轉換成十六進位制.
  • __index__(self)
  • 當物件被切片時轉換成int型。如果你定義了一個可能被用來做切片操作的數值型,你就應該定義__index__.
  • __trunc__(self)
  • 當 math.trunc(self) 使用時被呼叫.__trunc__返回自身型別的整型擷取 (通常是一個長整型).
  • __coerce__(self, other)
  • 執行混合型別的運算,如果轉換不能完成,應該返回None;否則,要返回一對兩個元數的元組self和other, 被操作成同型別。

表示你的類

用一個字串來表示一個類往往會非常有用。在Python中,有很多你可以在類定義中實施的方法來自定義內建函式的返回值以表示出你所寫出的類的某些行為。

 

  • __str__(self)
  • 定義當 str() 被你的一個類的例項呼叫時所要產生的行為。

 

  • __repr__(self)
  • 定義 當 repr()  被你的一個類的例項呼叫時所要產生的行為。 str() 和 repr() 的主要區別是其目標群體。 repr() 返回的是機器可讀的輸出,而 str() 返回的是人類可讀的。

 

 

  • __unicode__(self)
  • 定義當 unicode() 被你的一個類的例項呼叫時所要產生的行為。 unicode() 和 str() 很相似,但是返回的是unicode字串。注意,如果對你的類呼叫 str() 然而你只定義了 __unicode__() ,那麼其將不會工作。你應該定義 __str__() 來確保呼叫時能返回正確的值,並不是每個人都有心情去使用unicode。
  • __format__(self, formatstr)
  • 定義當你的一個類的例項被用來用新式的格式化字串方法進行格式化時所要產生的行為。例如, “Hello, {0:abc}!”.format(a) 將會導致呼叫 a.__format__(“abc”) 。這對定義你自己的數值或字串型別是十分有意義的,你可能會給出一些特殊的格式化選項。

 

  • __hash__(self)
  • 定義當 hash()被你的一個類的例項呼叫時所要產生的行為。它返回一個整數,用來在字典中進行快速比較。請注意,這通常也承擔著實現__eq__。有下面這樣的規則:a == b 暗示著 hash(a) == hash(b) 。

 

 

  • __nonzero__(self)
  • 定義當 bool() 被你的一個類的例項呼叫時所要產生的行為。本方法應該返回True或者False,取決於你想讓它返回的值。

 

  • __dir__(self)
  • 定義當 dir() 被你的一個類的例項呼叫時所要產生的行為。該方法應該返回一個屬性的列表給使用者,一般而言,實現 __dir__ 是不必要的,但是,如果你重新定義了__getattr__或__getattribute__(你將在下一節中看到)或者其它的動態生成屬性,那麼它對你的類的互動使用是至關重要的。
  • __sizeof__(self)
  • 定義當 sys.getsizeof() 被你的一個類的例項呼叫時所要產生的行為。該方法應該以位元組為單位,返回你的物件的大小。這通常對於以C擴充套件的形式實現的Python類更加有意義,其有助於理解這些擴充套件。

我們幾乎完成了對這些枯燥的魔法方法(並且沒有例項)的指導。現在,我們已經提及到了一些較基本的魔法方法,到了該轉移到更高階內容的時候了。

屬性訪問控制

很多用過其它語言的人抱怨Python缺乏對類真正的封裝(比如沒辦法定義private屬性和public的getter和settter)。但這不是真的啊:真相是Python通過“魔法”實現了大量的封裝,而不是使用明確的方法或欄位修飾符。看一下吧:

  • __getattr__(self, name)
  • 你可以定義如何處理使用者試圖訪問一個不存在(不存在或還沒建立)屬性的行為。這對於捕獲或者重定向一般的拼寫錯誤非常有用,給出訪問了不能訪問的屬性的警告(如果你願意,你還可以推斷並返回那個屬性。),或者巧妙地處理一個AttributeError異常。它只有在一個不存在的屬性被訪問的情況下才被呼叫,然而,這並不是一個真正封裝的方案。
  • __setattr__(self, name, value)
  • 與__getattr__不同,__setattr__是一個真正的封裝方案。它允許你定義當給一個存在或不存在的屬性賦值時的行為,意味著對任何屬性值的改變你都可以定義一個規則。可是,你得小心使用__setattr__,在這個清單結尾的例子會向你說明。
  • __delattr__
  • 它與__setattr__非常像, 只不過是用來刪除而不是設定屬性。 __detattr__需要預防措施,就像setattr一樣,當被呼叫時可能會引起無限遞迴(當__delattr__已經實現時,呼叫 del self.name 就會引起無限的遞迴)。
  • __getattribute__(self, name)
  •  __getattribute__相當適合它的同伴__setattr__和__delattr__.但我卻不建議你使用它。__getattribute__只有在新風格的類中才會被使用(所有的新風格類在Python最新的版本中,在老版本中,你可以子類化object來獲得一個新風格類。它允許你定義一條規則來處理無論什麼時候屬性值被訪問時的行為。比如類似於由於其它的夥伴犯錯而引起的無限遞迴(這時你就可以呼叫基類的__getattribute__方法來阻止它)。它也避免了對__getattr__的依賴,當__getattribute__方法已經實現的時候,__getattr__只有在__getattribute__被明確的呼叫或丟擲一個AttributeError異常的時候才會被呼叫。這個方法能被使用(畢竟,這是你的選擇),但是我不推薦它,因為它很少使用並且執行的時候很難保證沒有BUG。

如果定義了任何屬性訪問控制方法,容易產生錯誤。思考下面這個例子:

再次證明了Python的魔法方法是難以置信的強大,但強大的力量也需要強大的責任。如果你不想執行時中斷你的程式碼,那瞭解如何適當地使用魔法方法就非常重要啦。

我們從Python中定製的屬性訪問中學到了什麼?它們不是被輕易使用的。事實上,它有點過分強大並且違反直覺。但它們存在的原因是用來止癢的:Python不阻止你製造遭糕東西,但可能會讓它變的困難。自由是最重要的東西,所以你可做任何你想做的事情。這裡有一個例子,展示了一些特殊的屬性訪問控制行為。(注意我們使用super,因為不是所有的類都有__dict__屬性):

自定義序列

有很多辦法能讓你的Python類使用起來就像內建的序列(dict,tuple,list,string等)。Python裡有一些目前我最喜歡的辦法,因為它們給你的控制到了荒謬的程度並且神奇地使得大量的全域性函式優雅地工作在你類的例項當中。但是在深入講這些好東西之前,我們先介紹下需求。

需求

在討論在Python中建立你自己的序列也是時候談談協議了。在其他語言中協議有點類似於介面,因為你必須實現一系列的方法。然而,在Python中協議是完全不正式的,不需要顯式的宣告去實現它,它更像是一種指導原則。

為什麼我們要談論協議呢?因為在Python中實現自定義容器型別涉及到這些協議的使用。首先,有一些協議用於定義不變容器:為了實現一個不變視窗,你只需定義__len__和__getitem__方法(接下來會細說)。不變容器的協議要求所有的類加上一個 __setitem__和__delitem__方法。最後,如果你想讓你的容器支援遍歷,你必須定義__iter__方法,它返回一個iterator。這個iterator必須遵守iterator的協議,它要求iterator類裡面有__iter__方法(返回自身)和next方法。

容器後的魔法

不需要再等待了,這裡就是容器所使用的一些魔法方法。

  • __len__(self)
  • 返回容器的長度。對於可變和不可變容器的協議,這都是其中的一部分。
  • __getitem__(self, key)
  • 定義當某一項被訪問時,使用self[key]所產生的行為。這也是不可變容器和可變容器協議的一部分。如果鍵的型別錯誤將產生TypeError;如果key沒有合適的值則產生KeyError。
  • __setitem__(self, key, value)
  • 定義當一個條目被賦值時,使用self[nkey] = value所產生的行為。這也是協議的一部分。而且,在相應的情形下也會產生KeyError和TypeError。
  • __delitem__(self, key)
  • 定義當某一項被刪除時所產生的行為。(例如del self[key])。這只是可變容器協議的一部分。當你使用一個無效的鍵時必須丟擲適當的異常。
  • __iter__(self)
  • 返回一個容器迭代器,很多情況下會返回迭代器,尤其是當內建的iter()方法被呼叫的時候,以及當使用for x in container:方式迴圈的時候。迭代器是它們本身的物件,它們必須定義返回self的__iter__方法。
  • __reversed__(self)
  • 實現當reversed()被呼叫時的行為。應該返回序列反轉後的版本。僅當序列可以是有序的時候實現它,例如對於列表或者元組。
  • __contains__(self, item)
  • 定義了呼叫in和not in來測試成員是否存在的時候所產生的行為。你可能會問為什麼這個不是序列協議的一部分?因為當__contains__沒有被定義的時候,Python會迭代這個序列,並且當找到需要的值時會返回True。
  • __missing__(self, key)
  • 其在dict的子類中被使用。它定義了當一個不存在字典中的鍵被訪問時所產生的行為。(例如,如果我有一個字典d,當”george”不是字典中的key時,使用了d[“george”],此時d[“george”]將會被呼叫)。

一個例子

對於我們的例子, 讓我們看看一個列表,它實現了一些功能結構,你可能在其他在其他程式中用到 (例如Haskell).

反射

你也可以控制怎麼使用內建在函式sisinstance()和issubclass()方法 反射定義魔法方法. 這個魔法方法是:

  • __instancecheck__(self, instance)
  • 檢查物件是否是您定義的類的一個例項(例.isinstance(instance, class).
  • __subclasscheck__(self, subclass)
  • 檢查類是否是你定義類的子類 (例.issubclass(subclass, class)).

這些魔法方法的用例看起來很小, 並且確實非常實用. 我不想花太多時間在反射魔法方法上,因為它們不是非常重要, 但是它們反應了關於物件導向程式上一些重要的東西在Python上,並且總的來說Python: 總是一個簡單的方法去找某些事情, 即使是沒有必要的. 這些魔法方法可能看起來不是很有用, 但是一旦你需要它們,你會感到慶幸它們的存在 (並且為自己閱讀了本指南高興!).

可呼叫物件

你也許已經知道,在Python中,方法是最高階的物件。這意味著他們也可以被傳遞到方法中,就像其他物件一樣。這是一個非常驚人的特性。

在Python中,一個特殊的魔法方法可以讓類的例項的行為表現的像函式一樣,你可以呼叫它們,將一個函式當做一個引數傳到另外一個函式中等等。這是一個非常強大的特性,其讓Python程式設計更加舒適甜美。

  • __call__(self, [args…])
  • 允許一個類的例項像函式一樣被呼叫。實質上說,這意味著 x() 與 x.__call__() 是相同的。注意 __call__ 的引數可變。這意味著你可以定義 __call__ 為其他你想要的函式,無論有多少個引數。

__call__ 在那些類的例項經常改變狀態的時候會非常有效。“呼叫”這個例項是一種改變這個物件狀態的直接和優雅的做法。比如這樣一個例子,一個類表示了一個實體在飛機上的位置:

會話管理器

在Python 2.5中,為了程式碼重用而新定義了一個關鍵字with,其也就帶來了一種with語句。會話管理在Python中並不罕見(之前是作為庫的一部分而實現的),不過直到PEP 343被接受後,其就作為了一種一級語言結構。你也許在之前看到過這樣的語句:

會話管理器通過包裝一個with語句來設定和清理相應物件的行為。會話管理器的行為通過兩個魔方方法來決定:

  • __enter__(self)
  • 定義了當使用with語句的時候,會話管理器在塊被初始建立事要產生的行為。請注意,__enter__的返回值與with語句的目標或者as後的名字繫結。
  • __exit__(self, exception_type, exception_value, traceback)
  • 定義了當一個程式碼塊被執行或者終止後,會話管理器應該做什麼。它可以被用來處理異常、執行清理工作或做一些程式碼塊執行完畢之後的日常工作。如果程式碼塊執行成功,exception_type,exception_value,和traceback將會為None。否則,你可以選擇處理這個異常或者是直接交給使用者處理。如果你想處理這個異常的話,請確保__exit__在所有語句結束之後返回True。如果你想讓異常被會話管理器處理的話,那麼就讓其產生該異常。

__enter__和__exit__對於那些定義良好以及有普通的啟動和清理行為的類是很有意義的。你也可以使用這些方法來建立一般的可以包裝其它物件的會話管理器。下面是一個例子:

下面是一個實際使用Closer的例子,使用一個FTP連線來證明(一個可關閉的套接字):

看到我們的包裝器如何友好地處理恰當和不不恰當的行為了嗎?這是會話管理器和魔法方法的強大功能。請注意,Python標準庫包括了一個叫作 contextlib 的模組,其包含了一個會話管理器,contextlib.closing()完成了類似的功能(當一個物件沒有close()方法時則沒有任何處理)。

抽象基類

見http://docs.python.org/2/library/abc.html。

建立描述器物件

描述器是通過獲取、設定以及刪除的時候被訪問的類。當然也可以改變其它的物件。描述器並不是獨立的。相反,它意味著被一個所有者類持有。當建立物件導向的資料庫或者類,裡面含有相互依賴的屬相時,描述器將會非常有用。一種典型的使用方法是用不同的單位表示同一個數值,或者表示某個資料的附加屬性(比如座標系上某個點包含了這個點到原點的距離資訊)。

為了成為一個描述器,一個類必須至少有__get__,__set__,__delete__方法被實現,讓我們看看這些魔法方法:

  • __get__(self, instance, owner)
  • 定義了當描述器的值被取得的時候的行為。instance是擁有該描述器物件的一個例項。owner是擁有者本身。
  • __set__(self, instance, value)
  • 定義了當描述器的值被改變的時候的行為。instance是擁有該描述器類的一個例項。value是要設定的值。

 

  • __delete__(self, instance)
  • 定義了當描述器的值被刪除的時候的行為。instance是擁有該描述器物件的一個例項。

 

下面是一個描述器的例項:單位轉換。

複製

有時候,尤其是當你在處理可變物件時,你可能想要複製一個物件,然後對其做出一些改變而不希望影響原來的物件。這就是Python的copy所發揮作用的地方。然而(幸運的是),Python的模組並不是“感性”的,所以我們沒必要擔心一個基於Linux的機器會突然開始工作,但是我們確實需要告訴Python如何高效地複製一些東西。

  • __copy__(self)
  • 定義了當對你的類的例項呼叫copy.copy()時所產生的行為。copy.copy()返回了你的物件的一個淺拷貝——這意味著,當例項本身是一個新例項時,它的所有資料都被引用了——例如,當一個物件本身被複制了,它的資料仍然是被引用的(因此,對於淺拷貝中資料的更改仍然可能導致資料在原始物件的中的改變)。
  • __deepcopy__(self, memodict={})
  • 定義了當對你的類的例項呼叫copy.deepcopy()時所產生的行為。copy.deepcopy()返回了你的物件的一個深拷貝——物件和其資料都被拷貝了。memodict是對之前被拷貝的物件的一個快取——這優化了拷貝過程並且阻止了對遞迴資料結構拷貝時的無限遞迴。當你想要進行對一個單獨的屬性進行深拷貝時,呼叫copy.deepcopy(),並以memodict為第一個引數。

這些魔法方法的使用例子都是什麼?答案和以往一樣,當你需要進行和預設行為相比,更細粒度的控制時使用這些方法。例如,你想要複製一個物件,其中以字典的形式(其可能會很大)儲存了一個快取,那麼對快取進行復制可能是沒有意義的——如果當該快取可以在記憶體中被多個例項共享,那麼對其進行復制就確實是沒意義的。

Pickling 序列化你的物件

如果你打算與其他python發燒友交換資料,那你一定應該聽說過pickling。Pickling是一個將Python資料結構進行序列化的工具,它對於儲存、重新取回一個物件這類工作來說真是難以置信的有用。但它也是一些擔心和誤解的源頭。

Pickling是如此的重要,以至於它不僅僅擁有自己的模組(pickling),而且還有自己的協議和“魔術”方法。但首先,我們先簡單地介紹一下Pickling如何序列化已存在的型別(如果你已經知道這些了,那麼請自行飄過)。

Pickling: 趕快到鹽水中泡泡

(譯者注:pickle是用來直接儲存Python物件的模組,在英文中有“醃製”的意思)

讓我們深入挖掘pickling方法。假設你想儲存一個字典並在之後檢索它:你可以把它寫入一個檔案中,小心確保其有正確的語法,之後用exec()或者讀取檔案來檢索它。但這很有可能是相當危險的:如果你將重要資料儲存在純文字中,它可能會損壞或者發生各種各樣的改變,有些會讓你的程式崩潰,有些甚至會在你的電腦上執行惡意程式碼。因此,我們應該使用 pickle方法:

幾個小時之後,我們希望找回這些資料,現在我們只需unpickle它:

發生了什麼?正如你所想的那樣,我們現在找回了data。

現在,我們要注意一點:pickle並不完美。被pickle序列化的檔案很容易被意外或是有意損壞。pickle模組可能比一般的純文字檔案要來的安全,但它仍然可能會被利用去執行惡意程式碼。而且它在各個Python版本之間是不相容的,所以不要傳送pkl檔案並妄想其他人可以開啟它。但是,pickle確實是處理快取和其他序列化任務的強有力工具。

用Pickle序列化你的物件

pickle模組不僅可以用於內建型別,它還可以以用於序列化任何遵循pickle協議的類。pickle協議為Python物件定義了四個可選的方法,你可以過載這些方法來定義它們的行為(這和C擴充套件有些不同,但這不在我們的討論範圍之內):

  • __getinitargs__(self)
  • 如果你想在你的類被unpickle的時候執行__init__方法,你可以過載__getinitargs__方法,它會返回一個元組,包含你想傳給__init__方法的引數。注意,這種方法只適用於舊式的Python型別(譯者注:區別於2.2中引入的新式類)。
  • __getnewargs__(self)
  • 對於新式類,在unpickle的時候你可以決定傳給__new__方法的引數。以上方法可以返回一個包含你想傳給__new__方法的引數元組。
  • __getstate__(self)
  • 除了儲存__dict__中的原來的那些變數,你可以自定義使用pickle序列化物件時想要儲存的額外屬性。這些屬性將在你unpickle檔案時被__setstate__方法使用。
  • __setstate__(self, state)
  • 當檔案被unpickle時,其中儲存的物件屬性不會直接被寫入物件的__dict中,而是會被傳入這個方法。這個方法和__getstate__是配套的:當他們都被定義了的時候,你可以任意定義物件被序列化儲存時的狀態。
  • __reduce__(self)
  • 當你定義擴充套件類(使用C語言實現的Python擴充套件類)時,可以通過實現__reduce__函式來控制pickle的資料。如果__reduce__()方法被定義了,在一個物件被pickle時它將被呼叫。如果它返回一個字串,那麼pickle在將在全域性空間中搜尋對應名字的物件進行pickle;它還可以返回一個元組,包含2-5個元素: 一個可以用來重建該物件的可呼叫物件,一個包含有傳給該可呼叫物件引數的元組,傳給__setstate__方法的引數(可選),一個用於待pickle物件列表的迭代器(譯者注:這些物件會被append到原來物件的後面)(可選)呼叫物件,一個包含有傳給該可呼叫物件引數的元組,傳給__setstate__方法的引數(可選),一個用於待pickle物件列表的迭代器(譯者注:這些物件會被append到原來物件的後面)(可選),一個用於待pickle的字典的迭代器(可選)。
  • __reduce_ex__(self)
  • __reduce_ex__是為相容性而設計的。如果它被實現了,__reduce_ex__將會取代__reduce__在pickle時被執行。__reduce__可以同時被實現以支援那些不支援__reduce_ex__的老版本pickling API。

(譯者注:這段說的不是非常清楚,感興趣可以去看文件,一般來說只要使用上一節中的方法就足夠了,注意在反序列化之前要先有物件的定義,否則會出錯)

一個例子

我們以Slate為例,這一段記錄一個值以及這個值是何時被寫入的程式,但是,這個Slate有一點特殊的地方,就是當前值不會被儲存。

總結

這份指南的目的是希望為所有人帶來一些知識,即使你是Python大牛或者精通物件導向開發。如果你是一個Python初學者,閱讀這篇文章之後,你已經獲得了編寫豐富,優雅,靈活的類的知識基礎了。如果你是一個有一些經驗的Python程式設計師,你可能會發現一些能讓你寫的程式碼更簡潔的方法。如果你是一個曾經使用過Python的程式設計師,該文可能會幫助你知曉一些新的概念和方法以及幫助你減少編寫程式碼量的方式。如果你是一個Python專家,該文會幫助你想起來一些你已經遺忘的只是,或者一些你還沒聽說過的新功能。不慣你現在有多少經驗,我希望這次對於Python特殊方法的旅程是真正的一次神奇之旅。(雙關語的感覺真是棒!)

附錄 1: 如何呼叫Magic Method

一些magic method已經對映到自帶的方法(built-in functions);這種情況下如何呼叫他們是顯而易見的。然而,在其他情況下,呼叫它們就不那麼容易了。本附錄致力於展示能夠呼叫magic method的一些不被察覺的語法。

Magic Method 何時被呼叫(例子) Explanation
__new__(cls [,…]) instance = MyClass(arg1, arg2)  __new__ is called on instance creation
__init__(self [,…]) instance = MyClass(arg1, arg2) __init__ is called on instance creation
__cmp__(self, other) self == other, self > other, etc. Called for any comparison
__pos__(self) +self Unary plus sign
__neg__(self) -self Unary minus sign
__invert__(self) ~self Bitwise inversion
__index__(self) x[self] Conversion when object is used as index
__nonzero__(self) bool(self) Boolean value of the object
__getattr__(self, name) self.name # name doesn’t exist Accessing nonexistent attribute
__setattr__(self, name, val) self.name = val Assigning to an attribute
__delattr__(self, name) del self.name Deleting an attribute
__getattribute__(self, name) self.name Accessing any attribute
__getitem__(self, key) self[key] Accessing an item using an index
__setitem__(self, key, val) self[key] = val Assigning to an item using an index
__delitem__(self, key) del self[key] Deleting an item using an index
__iter__(self) for x in self Iteration
__contains__(self, value) value in self,value not in self Membership tests using in
__call__(self [,…]) self(args) “Calling” an instance
__enter__(self) with self as x: with statement context managers
__exit__(self, exc, val, trace) with self as x: with statement context managers
__getstate__(self) pickle.dump(pkl_file, self) Pickling
__setstate__(self) data = pickle.load(pkl_file) Pickling

希望這個表能夠解決你可能會遇到的哪個語法呼叫哪個magic method的問題。

附錄 2: Python 3中的改動 

這裡我們列舉出一些Python 3與2.x在物件模型上主要的的不同之處。

  • 因為Python 3中string和unicode直接已經沒有差別,__unicode__已經不存在了,並且__bytes__(它的行為與__str__和__unicode__類似)成為新的自帶方法來構造byte陣列。
  • 因為Python 3裡面的division預設變成了true division,__div__在Python3中不存在了。
  • __coerce__被去除掉了是因為它與其他magic method冗餘並且造成了行為混淆。
  • __cmp__被去除掉了是因為它與其他magic method冗餘。
  • __nonzero__被重新命名為__bool__

 

相關文章