Python 研究-魔術方法指南

Python 魔術方法指南
•    入門
•    構造和初始化
•    構造定製類
o    用於比較的魔術方法
o    用於數值處理的魔術方法
•    表現你的類
•    控制屬性訪問
•    新增定製序列
•    反射
•    可以呼叫的物件
•    會話管理器
•    新增描述器物件
•    持久化物件
•    總結
•    附錄
介紹
此教程為我的數篇文章中的一個重點。主題是魔術方法。 什麼是魔術方法?他們是物件導向的Python的一切。他們是可以給你的類增加」magic」的特殊方法。他們總是被雙下劃線所包圍(e.g. __init__ 或者 __lt__)。 然而他們的文件卻遠沒有提供應該有的內容。Python中所有的魔術方法均在Python官方文件中有相應描述,但是對於他們的描述比較混亂而且組織比較 鬆散。很難找到有一個例子(也許他們原本打算的很好,在開始語言參考中有描述很詳細,然而隨之而來的確是枯燥的語法描述等等)。
所以,為了修補我認為Python文件應該修補的瑕疵,我決定給Python中的魔術方法提供一些用平淡的語言和範例驅使的文件。我在開始已經寫了數篇博文,現在在這篇文章中對他們進行總結。
我希望你能夠喜歡這篇文章。你可以將之當做一個教程,一個補習資料,或者一個參考。本文章的目的僅僅是為Python中的魔術方法提供一個友好的教程。
構造和初始化
每個人都知道一個最基本的魔術方法, __init__ 。通過此方法我們可以定義一個物件的初始操作。然而,當我呼叫 x = SomeClass() 的時候, __init__ 並不是第一個被呼叫的方法。實際上,還有一個叫做 __new__ 的方法,來構造這個範例。然後給在開始新增時候的初始化函數來傳遞參數。在物件生命週期的另一端,也有一個 __del__ 方法。我們現在來近距離的看一看這三個方法:
__new__(cls, [...) __new__ 是在一個物件範例化的時候所呼叫的第一個方法。它的第一個參數是這個類,其他的參數是用來直接傳遞給 __init__ 方法。 __new__ 方法相當不常用,但是它有自己的特性,特別是當繼承一個不可變的類型比如一個tuple或者string。我不希望在 __new__ 上有太多細節,因為並不是很有用處,但是在 Python文件 中有詳細的闡述。
__init__(self, […) 此方法為類的初始化方法。當建構式被呼叫的時候的任何參數都將會傳給它。(比如如果我們呼叫 x = SomeClass(10, 'foo')),那麼 __init__ 將會得到兩個參數10和foo。 __init__ 在Python的類定義中被廣泛用到。
__del__(self) 如果 __new__ 和 __init__ 是物件的構造器的話,那麼 __del__ 就是析構器。它不實現語句 del x (以上程式碼將不會翻譯為 x.__del__() )。它定義的是當一個物件進行垃圾回收時候的行為。當一個物件在刪除的時需要更多的清潔工作的時候此方法會很有用,比如套接字物件或者是文件物件。注意,如果解釋器退出的時候物件還存存在,就不能保證 __del__ 能夠被執行,所以 __del__ can't serve as a replacement for good coding practices ()~~~~~~~
放在一起的話,這裡是一個 __init__ 和 __del__ 實際使用的例子。
from os.path import join

class FileObject:
    '''給文件物件進行包裝從而確認在刪除時文件流關閉'''

    def __init__(self, filepath='~', filename='sample.txt'):
        #讀寫模式打開一個文件
        self.file = open(join(filepath, filename), 'r+')

    def __del__(self):
        self.file.close()
        del self.file
讓定製的類工作起來
使用Python的魔術方法的最大優勢在於他們提供了一種簡單的方法來讓物件可以表現的像內置類型一樣。那意味著你可以避免醜陋的,違反直覺的,不標準的的操作方法。在一些語言中,有一些操作很常用比如:
if instance.equals(other_instance):
    # do something
在Python中你可以這樣。但是這會讓人迷惑且產生不必要的冗餘。相同的操作因為不同的庫會使用不同的名字,這樣會產生不必要的工作。然而有了魔術方法的力量,我們可以定義一個方法(本例中為 __eq__ ),就說明了我們的意思:
if instance == other_instance:
        #do something
這只是魔術方法的功能的一小部分。它讓你可以定義符號的含義所以我們可以在我們的類中使用。就像內置類型一樣。
用於比較的魔術方法
Python對實現物件的比較,使用魔術方法進行了大的逆轉,使他們非常直觀而不是笨拙的方法呼叫。而且還提供了一種方法可以重寫Python對物件比較的預設行為(通過引用)。以下是這些方法和他們的作用。
__cmp__(self, other) __cmp__ 是最基本的用於比較的魔術方法。它實際上實現了所有的比較符號(<,==,!=,etc.),但是它的表現並不會總是如你所願(比如,當一個範例與另一個範例相等是通過一個規則來判斷,而一個範例大於另外一個範例是通過另外一個規則來判斷)。如果 self < other 的話 __cmp__ 應該返回一個負數,當 self == other 的時候會返回0 ,而當 self > other 的時候會返回正數。通常最好的一種方式是去分別定義每一個比較符號而不是一次性將他們都定義。但是 __cmp__ 方法是你想要實現所有的比較符號而一個保持清楚明白的一個好的方法。
__eq__(self, other) 定義了等號的行為, == 。
__ne__(self, other) 定義了不等號的行為, != 。
__lt__(self, other) 定義了小於號的行為, < 。
__gt__(self, other) 定義了大於等於號的行為, >= 。
舉一個例子,新增一個類來表現一個詞語。我們也許會想要比較單詞的字典序(通過字母表),通過預設的字串比較的方法就可以實現,但是我們也想要通過一些其他的標準來實現,比如單詞長度或者音節數量。在這個例子中,我們來比較長度實現。以下是實現程式碼:
class Word(str):
'''存儲單詞的類,定義比較單詞的幾種方法'''

    def __new__(cls, word):
        # 注意我們必須要用到__new__方法,因為str是不可變類型
        # 所以我們必須在新增的時候將它初始化
        if ' ' in word:
            print "Value contains spaces. Truncating to first space."
            word = word[:word.index(' ')] #單詞是第一個空格之前的所有字符
        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)
現在,我們新增兩個 Words 物件(通過使用 Word('foo') 和 Word('bar') 然後通過長度來比較它們。注意,我們沒有定義 __eq__ 和 __ne__ 方法。這是因為將會產生一些怪異的結果(比如 Word('foo') == Word('bar') 將會返回true)。這對於測試基於長度的比較不是很有意義。所以我們退回去,用 str 內置來進行比較。
現在你知道你不必定義每一個比較的魔術方法從而進行豐富的比較。標準庫中很友好的在 functiontols 中提供給我們一個類的裝飾器定義了所有的豐富的比較函數。如果你只是定義 __eq__ 和另外一個(e.g. __gt__, __lt__,etc.)這個特性僅僅在Python 2.7中存在,但是你如果有機會碰到的話,那麼將會節省大量的時間和工作量。你可以通過在你定義的類前放置 @total_ordering 來使用。
數值處理的魔術方法
如同你在通過比較符來比較類的範例的時候來新增很多方法,你也可以定義一些數值符號的特性。繫緊你的安全帶,來吧,這裡有很多內容。為了組織方便,我將會把數值處理的方法來分成五類:一元運算子,普通算數運算子,反射算數運算子(之後會詳細說明),增量賦值,和類型轉換。
一元運算子和函數
僅僅有一個操作位的一元運算子和函數。比如絕對值,負等。
__pos__(self) 實現正號的特性(比如 +some_object)
__neg__(self) 實現負號的特性(比如 -some_object)
__abs__(self) 實現內置 abs() 函數的特性。
__invert__(self) 實現 ~ 符號的特性。為了說明這個特性。你可以查看 Wikipedia中的這篇文章
普通算數運算子
現在我們僅僅覆蓋了普通的二進制運算子:+,-,*和類似符號。這些符號大部分來說都淺顯易懂。
__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
所以,除了當與其他物件操作的時候自己會成為第二個操作數之外,所有的這些魔術方法都與普通的操作是相同的。大多數情況下,反運算的結果是與普通運算相同的。所以你可以你可以將 __radd__ 與 __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(other, self) 被呼叫時,實現內置 divmod() 的反運算 __rpow__ 實現 ** 符號的反運算 __rlshift__(self, other) 實現 << 符號的反左位移 __rrshift__(self, other) 實現 >> 符號的反右位移 __rand__(self, other) 實現 & 符號的反與運算 __ror__(self, other) 實現 | 符號的反或運算 __xor__(self, other) 實現 ^ 符號的反異或運算
增量賦值
Python也有大量的魔術方法可以來定製增量賦值語句。你也許對增量賦值已經很熟悉,它將運算子與賦值來結合起來。如果你仍然不清楚我在說什麼的話,這裡有一個例子:
x = 5
x += 1 # in other words x = x + 1
__iadd__(self, other) 實現賦值加法 __isub__(self, other) 實現賦值減法 __imul__(self, other) 實現賦值乘法 __ifloordiv__(self, other) 實現 //= 的賦值地板除 __idiv__(self, other) 實現符號 /= 的賦值除 __itruediv__(self, other) 實現賦值真除,只有使用 from __future__ import division 的時候才能使用 __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) 當物件是被套用在切片表達式中時,實現整形強制轉換,如果你定義了一個可能在切片時用到的定製的數值型,你應該定義 __index__ (詳見PEP357) __trunc__(self) 當使用 math.trunc(self) 的時候被呼叫。 __trunc__ 應該返回數值被截取成整形(通常為長整形)的值 __coerce__(self, other) 實現混合模式算數。如果類型轉換不可能的話,那麼 __coerce__ 將會返回 None ,否則他將對 self 和 other 返回一個長度為2的tuple,兩個為相同的類型。
表現你的類
如果有一個字串來表示一個類將會非常有用。在Python中,有很多方法可以實現類定義內置的一些函數的返回值。 __str__(self) 定義當 str() 呼叫的時候的返回值 __repr__(self) 定義 repr() 被呼叫的時候的返回值。 str() 和 repr() 的主要區別在於 repr() 返回的是機器可讀的輸出,而 str() 返回的是人類可讀的。 __unicode__(self) 定義當 unicode() 呼叫的時候的返回值。 unicode() 和 str() 很相似,但是返回的是unicode字串。注意,如a果對你的類呼叫 str() 然而你只定義了 __unicode__() ,那麼將不會工作。你應該定義 __str__() 來確保呼叫時能返回正確的值。
__hash__(self) 定義當 hash() 呼叫的時候的返回值,它返回一個整形,用來在字典中進行快速比較 __nonzero__(self) 定義當 bool() 呼叫的時候的返回值。本方法應該返回True或者False,取決於你想讓它返回的值。
控制屬性訪問
許多從其他語言轉到Python的人會抱怨它缺乏類的真正封裝。(沒有辦法定義私有變數,然後定義公共的getter和setter)。Python其實可以通過魔術方法來完成封裝。我們來看一下:
__getattr__(self, name) 你可以定義當用戶試圖獲取一個不存在的屬性時的行為。這適用於對普通拼寫錯誤的獲取和重定向,對獲取一些不建議的屬性時候給出警告(如果你願意你也可以計算並且給出一個值)或者處理一個 AttributeError 。只有當呼叫不存在的屬性的時候會被返回。然而,這不是一個封裝的解決方案。 __setattr__(self, name, value) 與 __getattr__ 不同, __setattr__ 是一個封裝的解決方案。無論屬性是否存在,它都允許你定義對對屬性的賦值行為,以為這你可以對屬性的值進行個性定製。但是你必須對使用 __setattr__ 特別小心。之後我們會詳細闡述。 __delattr__ 與 __setattr__ 相同,但是功能是刪除一個屬性而不是設置他們。注意與 __setattr__ 相同,防止無限遞歸現象發生。(在實現 __delattr__ 的時候呼叫 del self.name 即會發生) __getattribute__(self, name) __getattribute__ 與它的同伴 __setattr__ 和 __delattr__ 配合非常好。但是我不建議使用它。只有在新類型類定義中才能使用 __getattribute__ (在最新版本Python中所有的類都是新類型,在老版本中你可以通過繼承 object 來製作一個新類。這樣你可以定義一個屬性值的訪問規則。有時也會產生一些帝歸現象。(這時候你可以呼叫父類的 __getattribute__ 方法來防止此現象的發生。)它可以消除對 __getattr__ 的使用,如果它被明確呼叫或者一個 AttributeError 被拋出,那麼當實現 __getattribute__ 之後才能被呼叫。此方法是否被使用其實最終取決於你的選擇。)我不建議使用它因為它的使用幾率較小(我們在取得一個值而不是設置一個值的時候有特殊的行為是非常罕見的。)而且它不能避免會出現bug。
在進行屬性訪問控制定義的時候你可能會很容易的引起一個錯誤。考慮下面的例子。
def __setattr__(self, name, value):
    self.name = value
    #每當屬性被賦值的時候, ``__setattr__()`` 會被呼叫,這樣就造成了遞歸呼叫。
    #這意味這會呼叫 ``self.__setattr__('name', value)`` ,每次方法會呼叫自己。這樣會造成程式崩潰。

def __setattr__(self, name, value):
    self.__dict__[name] = value  #給類中的屬性名分配值
    #定製特有屬性
Python的魔術方法非常強大,然而隨之而來的則是責任。瞭解正確的方法去使用非常重要。
所以我們對於定製屬性訪問權限瞭解了多少呢。它不應該被輕易的使用。實際上,它非常強大。但是它存在的原因是:Python 不會試圖將一些不好的東西變得不可能,而是讓它們難以實現。自由是至高無上的,所以你可以做任何你想做的。以下是一個特別的屬性控制的例子(我們使用 super 因為不是所有的類都有 __dict__ 屬性):
class AccessCounter:
    '''一個包含計數器的控制權限的類每當值被改變時計數器會加一'''

    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)
    #如果你不想讓其他屬性被訪問的話,那麼可以拋出 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中新增定製的序列,這個時候該講一講協議。協議(Protocols)與其他語言中的介面很相似。它給你很多你必須定義的方法。然而在Python中的協議是很不正式的,不需要明確聲明實現。事實上,他們更像一種指南。
我們為什麼現在討論協議?因為如果要定製容器類型的話需要用到這些協議。首先,實現不變容器的話有一個協議:實現不可變容器,你只能定義 __len__ 和 __getitem__ (一會會講更多)。可變容器協議則需要所有不可變容器的所有另外還需要 __setitem__ 和 __delitem__ 。最終,如果你希望你的物件是可迭代的話,你需要定義 __iter__ 會返回一個迭代器。迭代器必須遵循迭代器協議,需要有 __iter__ (返回它本身) 和 next 。
容器後的魔法
這些是容器使用的魔術方法。 __len__(self) 然會容器長度。對於可變不可變容器都需要有的協議的一部分。 __getitem__(self, key) 定義當一個條目被訪問時,使用符號 self[key] 。這也是不可變容器和可變容器都要有的協議的一部分。如果鍵的類型錯誤和 KeyError 或者沒有合適的值。那麼應該拋出適當的 TypeError 異常。 __setitem__(self, key, value) 定義當一個條目被賦值時的行為,使用 self[key] = value 。這也是可變容器和不可變容器協議中都要有的一部分。 __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__ 被定義。你問為什麼這個不是序列協議的一部分?那是因為當 __contains__ 沒有被定義的時候,Python會迭代這個序列並且當找到需要的值時會返回 True 。 __concat__(self, other) 最終,你可以通過 __concat__ 來定義當用其他的來連接兩個序列時候的行為。當 + 運算子被呼叫時候會返回一個 self 和 other.__concat__ 被呼叫後的結果產生的新序列。
一個例子
在我們的例子中,讓我們看一看你可能在其他語言中 用到的函數構造語句的實現(比如 Haskell)。
class FunctionalList:
'''一個封裝了一些附加魔術方法比如 head, tail, init, last, drop, 和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):
    #如果鍵的類別型或者值無效,列表值將會拋出錯誤
    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 reversed(self.values)

def append(self, value):
    self.values.append(value)
def head(self):
    return self.values[0]
def tail(self):
    return self.values[1:]
def init(self):
    #返回一直到末尾的所有元素
    return self.values[:-1]
def last(self):
    #返回末尾元素
    return self.values[-1]
def drop(self, n):
    #返回除前n個外的所有元素
    return self.values[n:]
def take(self, n):
    #返回前n個元素
    return self.values[:n]
反射
你可以通過魔術方法控制控制使用 isinstance() 和 issubclass() 內置方法的反射行為。這些魔術方法是:
__instancecheck__(self, instance)
檢查一個範例是不是你定義的類的範例
__subclasscheck__(self, subclass)
檢查一個類是不是你定義的類的子類
這些方法的用例似乎很少,這也許是真的。我不會花更多的時間在這些魔術方法上因為他們並不是很重要,但是他們的確反應了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):
    '''改變實體的位置'''
    self.x, self.y = x, y
會話管理
在Python 2.5中,為了程式碼利用定義了一個新的關鍵詞 with 語句。會話控制在Python中不罕見(之前是作為庫的一部分被實現),直到 PEP343 被增加後。它被成為一級語言結構。你也許之前看到這樣的語句:
with open('foo.txt') as bar:
# perform some action with 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鏈接來證明(一個可關閉的套接字):
>>> from magicmethods import Closer
>>> from ftplib import FTP
>>> with Closer(FTP('ftp.somesite.com')) as conn:
...     conn.dir()
...
>>> conn.dir()
>>> with Closer(int(5)) as i:
...     i += 1
...
Not closable.
>>> i
6
你已經看到了我們的包裝器如何靜默的處理適當和不適當的使用行為。這是會話管理器和魔術方法的強大功能。
新增物件的描述器
描述器是通過得到,設置,刪除的時候被訪問的類。當然也可以修改其他的物件。描述器並不是鼓勵的,他們注定被一個所有者類所持有。當新增物件導向的 資料庫或者類,裡面含有相互依賴的屬性時,描述器將會非常有用。一種典型的使用方法是用不同的單位表示同一個數值,或者表示某個資料的附加屬性(比如坐標 繫上某個點包含了這個點到遠點的距離訊息)。
為了構建一個描述器,一個類必須有至少 __get__ 或者 __set__ 其中一個,並且 __delete__ 被實現。讓我們看看這些魔術方法。 __get__(self, instance, owner) 定義當描述器的值被取得的時候的行為, instance 是擁有者物件的一個範例。 owner 是擁有者類本身。 __set__(self, instance, value) 定義當描述器值被改變時候的行為。 instance 是擁有者類的一個範例 value 是要設置的值。 __delete__(self, instance) 定義當描述器的值被刪除的行為。instance 是擁有者物件的範例。 以下是一個描述器的範例:單位轉換。
class Meter(object):
'''Descriptor for a meter.'''

    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):
    '''Descriptor for a foot.'''

    def __get__(self, instance, owner):
    return instance.meter * 3.2808
    def __set__(self, instance, value):
    instance.meter = float(value) / 3.2808

class Distance(object):
    '''Class to represent distance holding two descriptors for feet and
    meters.'''
    meter = Meter()
    foot = Foot()
儲存你的物件
如果你接觸過其他的 Pythoner,你可能已經聽說過 Pickle 了, Pickle 是用來序列化 Python 資料結構的模組,在你需要暫時存儲一個物件的時候(比如緩存),這個模組非常的有用,不過這同時也是隱患的誕生地。
序列化資料是一個非常重要的功能,所以他不僅僅擁有相關的模組( Pickle , cPickle ),還有自己的協議以及魔術方法,不過首先,我們先討論下關於序列化內建資料結構的方法。
Pickling: 簡單例子
讓我們深入研究 Pickle,比如說你現在需要臨時儲存一個字典,你可以把它寫入到一個文件裡,並且要小心翼翼的確保格式正確,之後再用 exec() 或者處理文件輸入來恢複數據,實際上這是很不安全的,如果你使用文本存儲了一些重要的資料,任何方式的改變都可能會影響到你的程式,輕則程式崩潰,重則被 惡意程式利用,所以,讓我們用 Pickle 代替這種方式:
import pickle

data = {'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 jar
jar.close()
嗯,過了幾個小時之後,我們需要用到它了,只需把它 unpickle 了就行了:
import pickle

pkl_file = open('data.pkl', 'rb') # connect to the pickled data
data = pickle.load(pkl_file) # load it into a variable
print data
pkl_file.close()
正如你期望的,資料原封不動的回來了!
同時要給你一句忠告: pickle 並不是很完美, Pickle 文件很容易被不小心或者故意損壞, Pickle 文件比純文本文件要稍微安全一點,但是還是可以被利用執行惡意程式。 Pickle 不是跨版本兼容的(譯註:最近剛好在 《Python Cookbook》上看到相關討論,書中描述的 Pickle 是跨版本兼容的,此點待驗證),所以儘量不要去分發 Pickle 過的文本,因為別人並不一定能夠打開。不過在做緩存或者其他需要序列化資料的時候, Pickle 還是很有用處的。
序列化你自己的物件
Pickle 並不是只支持內建資料結果,任何遵循 Pickle 協議的類都可以,Pickle 協議為 Python 物件規定了4個可選方法來自定義 Pickle 行為(對於 C 擴展的 cPickle 模組會有一些不同,但是這並不在我們的討論範圍內):
__getinitargs__(self)
如果你希望在逆序列化的同時呼叫 __init__ ,你可以定義 __getinitargs__ 方法,這個方法應該返回一系列你想被 __init__ 呼叫的參數,注意這個方法只對老樣式的類起作用。
__getnewargs__(self)
對於新式的類,你可以定義任何在重建物件時候傳遞到 __new__ 方法中的參數。這個方法也應該返回一系列的被 __new__ 呼叫的參數。
__getstate__(self)
你可以自定義當物件被序列化時返回的狀態,而不是使用 __dict 方法,當逆序列化物件的時候,返回的狀態將會被 __setstate__ 方法呼叫。
__setstate__(self, state)
在物件逆序列化的時候,如果 __setstate__ 定義過的話,物件的狀態將被傳給它而不是傳給 __dict__ 。這個方法是和 __getstate__ 配對的,當這兩個方法都被定義的時候,你就可以完全控制整個序列化與逆序列化的過程了。
例子
我們以 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 特殊方法的旅程能夠帶給你一些幫助(用雙關語真的很不錯 XD)(譯註: 這裡的雙關在於標題為 Magic Methods 這裡是 神奇的旅程 ,不過由於中英語序的問題,直譯略顯頭重腳輕,所以稍微變化了下意思,丟掉了雙關的含義)。
附錄:如何呼叫魔術方法
一些魔術方法直接和內建函數相對,在這種情況下,呼叫他們的方法很簡單,但是,如果是另外一種不是特別明顯的呼叫方法,這個附錄介紹了很多並不是很明顯的魔術方法的呼叫形式。
魔術方法    呼叫方式    解釋
__new__(cls [,...])    instance = MyClass(arg1, arg2)    __new__ 在新增範例的時候被呼叫
__init__(self [,...])    instance = MyClass(arg1, arg2)    __init__ 在新增範例的時候被呼叫
__cmp__(self, other)    self == other, self > other, 等。    在比較的時候呼叫
__pos__(self)    +self    一元加運算子
__neg__(self)    -self    一元減運算子
__invert__(self)    ~self    取反運算子
__index__(self)    x[self]    物件被作為索引使用的時候
__nonzero__(self)    bool(self)    物件的布爾值
__getattr__(self, name)    self.name # name 不存在    訪問一個不存在的屬性時
__setattr__(self, name, val)    self.name = val    對一個屬性賦值時
__delattr__(self, name)    del self.name    刪除一個屬性時
__getattribute(self, name)    self.name    訪問任何屬性時
__getitem__(self, key)    self[key]    使用索引訪問元素時
__setitem__(self, key, val)    self[key] = val    對某個索引值賦值時
__delitem__(self, key)    del self[key]    刪除某個索引值時
__iter__(self)    for x in self    迭代時
__contains__(self, value)    value in self, value not in self    使用 in 操作測試關係時
__concat__(self, value)    self + other    連接兩個物件時
__call__(self [,...])    self(args)    「呼叫」物件時
__enter__(self)    with self as x:    with 語句環境管理
__exit__(self, exc, val, trace)    with self as x:    with 語句環境管理
__getstate__(self)    pickle.dump(pkl_file, self)    序列化
__setstate__(self)    data = pickle.load(pkl_file)    序列化
希望這個表格對你對於什麼時候應該使用什麼方法這個問題有所幫助。

留言

這個網誌中的熱門文章

c語言-關於#define用法

PHP教學 - 資料型態(Data Type) - 上

CMD常用網管指令