Python 的 Magic Methods 指南

什麼是魔術方法呢?它們是面向對象Python語言中的一切。它們是你可以自定義並添加“魔法”到類中的特殊方法。它們被雙下劃線環繞(比如__init__或__lt__)。它們的文檔也不像它所需要的那麼齊備。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__ 使用的例子:

01 from os.path import joinclass FileObject:
02     '''Wrapper for file objects to make sure the file gets closed on deletion.'''
03  
04     def __init__(self, filepath='~', filename='sample.txt'):
05         # open a file filename in filepath in read and write mode
06         self.file = open(join(filepath, filename), 'r+')
07  
08     def __del__(self):
09         self.file.close()
10         del self.file

定義自己的類中的操作

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

1 if instance.equals(other_instance):
2     # do something

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

1 if instance == other_instance:
2     #do something

這是“魔法”方法強大用途的一部分。他們絕大部分讓我們定義操作的意義,以至於我們可以使用他們在我們自己的類中就像使用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)

  • 定義大於等於符號的行爲,>=

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

class Word(str):
    '''Class for words, defining comparison based on word length.'''

    def __new__(cls, word):
        # Note that we have to use __new__. This is because str is an immutable
        # type, so we have to initialize it early (at creation)
        if ' ' in word:
            print "Value contains spaces. Truncating to first space."
            word = word[:word.index(' ')] # Word is now all chars before first space
        return str.__new__(cls, word)

    def __gt__(self, other):
        return len(self) > len(other)
    def __lt__(self, other):
        return len(self) < len(other)
    def __ge__(self, other):
        return len(self) >= len(other)
    def __le__(self, other):
        return len(self) <= len(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)

    • 實現一個異或操作(^)的行爲

反射算術運算符

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

some_object + other

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

other + some_object

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

x = 5x += 1 # in other words x = x + 1

每一個方法的返回值都會被賦給左邊的變量。(比如,對於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。 

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

def __setattr__(self, name, value):
    self.name = value
    # since every time an attribute is assigned, __setattr__() is called, this
    # is recursion.
    # so this really means self.__setattr__('name', value). Since the method
    # keeps calling itself, the recursion goes on forever causing a crashdef __setattr__(self, name, value):
    self.__dict__[name] = value # assigning to the dict of names in the class
    # define custom behavior here

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

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

class AccessCounter(object):
    '''A class that contains a value and implements an access counter.    The counter increments each time the value is changed.'''

    def __init__(self, val):
        super(AccessCounter, self).__setattr__('counter', 0)
        super(AccessCounter, self).__setattr__('value', val)

    def __setattr__(self, name, value):
        if name == 'value':
            super(AccessCounter, self).__setattr__('counter', self.counter + 1)
        # Make this unconditional.
        # If you want to prevent other attributes to be set, raise AttributeError(name)
        super(AccessCounter, self).__setattr__(name, value)

    def __delattr__(self, name):
        if name == 'value':
            super(AccessCounter, self).__setattr__('counter', self.counter + 1)
        super(AccessCounter, self).__delattr__(name)]

自定義序列

有很多辦法能讓你的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).

class FunctionalList:
    '''A class wrapping a list with some extra functional magic, like head,    tail, init, last, drop, and take.'''

    def __init__(self, values=None):
        if values is None:
            self.values = []
        else:
            self.values = values

    def __len__(self):
        return len(self.values)

    def __getitem__(self, key):
        # if key is of invalid type or value, the list values will raise the error
        return self.values[key]

    def __setitem__(self, key, value):
        self.values[key] = value

    def __delitem__(self, key):
        del self.values[key]

    def __iter__(self):
        return iter(self.values)

    def __reversed__(self):
        return FunctionalList(reversed(self.values))

    def append(self, value):
        self.values.append(value)
    def head(self):
        # get the first element
        return self.values[0]
    def tail(self):
        # get all elements after the first
        return self.values[1:]
    def init(self):
        # get elements up to the last
        return self.values[:-1]
    def last(self):
        # get last element
        return self.values[-1]
    def drop(self, n):
        # get all elements except first n
        return self.values[n:]
    def take(self, n):
        # get first n elements
        return self.values[:n]

這樣你擁有了它,如何實現自己的序列的,有點用的例子。當然,也有更有用的應用程序的自定義序列,但在標準庫中,已經有相當多的實現(包括電池,對吧?),像Counter,OrderedDict,和NamedTuple。

反射

你也可以控制怎麼使用內置在函數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__ 在那些類的實例經常改變狀態的時候會非常有效。“調用”這個實例是一種改變這個對象狀態的直接和優雅的做法。比如這樣一個例子,一個類表示了一個實體在飛機上的位置:

class Entity:
    '''表示一個實體的類。調用該類以更新實體的位置。'''

    def __init__(self, size, x, y):
        self.x, self.y = x, y
        self.size = size

    def __call__(self, x, y):
        '''Change the position of the entity.'''
        self.x, self.y = x, y

    # snip...

會話管理器

在Python 2.5中,爲了代碼重用而新定義了一個關鍵字with,其也就帶來了一種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)

  • 定義了當一個代碼塊被執行或者終止後,會話管理器應該做什麼。它可以被用來處理異常、執行清理工作或做一些代碼塊執行完畢之後的日常工作。如果代碼塊執行成功,exception_type,exception_value,和traceback將會爲None。否則,你可以選擇處理這個異常或者是直接交給用戶處理。如果你想處理這個異常的話,請確保__exit__在所有語句結束之後返回True。如果你想讓異常被會話管理器處理的話,那麼就讓其產生該異常。

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

class Closer:
    '''通過with語句和一個close方法來關閉一個對象的會話管理器。'''

    def __init__(self, obj):
        self.obj = obj

    def __enter__(self):
        return self.obj # bound to target

    def __exit__(self, exception_type, exception_val, trace):
        try:
           self.obj.close()
        except AttributeError: # obj isn't closable
           print 'Not closable.'
           return True # exception handled successfully

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

01 >>> from magicmethods import Closer
02 >>> from ftplib import FTP
03 >>> with Closer(FTP('ftp.somesite.com')) as conn:
04 ...     conn.dir()
05 ...
06 >>> conn.dir()
07 >>> with Closer(int(5)) as i:
08 ...     i += 1
09 ...
10 Not closable.
11 >>> i
12 6

看到我們的包裝器如何友好地處理恰當和不不恰當的行爲了嗎?這是會話管理器和魔法方法的強大功能。請注意,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是擁有該描述器對象的一個實例。

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

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, value):
        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.2808class Distance(object):
    '''用米和英寸來表示兩個描述器之間的距離。'''
    meter = Meter()
    foot = Foot()

複製

有時候,尤其是當你在處理可變對象時,你可能想要複製一個對象,然後對其做出一些改變而不希望影響原來的對象。這就是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方法:

import pickledata = {'foo': [1, 2, 3],
        'bar': ('Hello', 'world!'),
        'baz': True}jar = open('data.pkl', 'wb')pickle.dump(data, jar) # write the pickled data to the file jarjar.close()

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

import picklepkl_file = open('data.pkl', 'rb') # connect to the pickled datadata = pickle.load(pkl_file) # load it into a variableprint datapkl_file.close()

發生了什麼?正如你所想的那樣,我們現在找回了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有一點特殊的地方,就是當前值不會被保存。

import time

class Slate:
    '''Class to store a string and a changelog, and forget its value when    pickled.'''

    def __init__(self, value):
        self.value = value
        self.last_change = time.asctime()
        self.history = {}

    def change(self, new_value):
        # Change the value. Commit last value to history
        self.history[self.last_change] = self.value
        self.value = new_value
        self.last_change = time.asctime()

    def print_changes(self):
        print 'Changelog for Slate object:'
        for k, v in self.history.items():
            print '%s\t %s' % (k, v)

    def __getstate__(self):
        # Deliberately do not return self.value or self.last_change.
        # We want to have a "blank slate" when we unpickle.
        return self.history

    def __setstate__(self, state):
        # Make self.history = state and last_change and value undefined
        self.history = state
        self.value, self.last_change = None, None

總結

這份指南的目的是希望爲所有人帶來一些知識,即使你是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__


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章