當前位置: 首頁 ? 深入 Python 3 ?

難度級別: ?????

特殊方法名稱

? My specialty is being right when other people are wrong. ?
George Bernard Shaw

 

深入

在本書其它幾處,我們已經見識過一些特殊方法——即在使用某些語法時 Python 所調用的“神奇”方法。使用特殊方法,類用起來如同序列、字典、函數、迭代器,或甚至像個數字!本附錄為我們已經見過特殊方法提供了參考,并對一些更加深奧的特殊方法進行了簡要介紹。

基礎知識

如果曾閱讀 《類的簡介》一章,你可能已經見識過了最常見的特殊方法: __init__() 方法。蓋章結束時,我寫的類多數需要進行一些初始化工作。還有一些其它的基礎特殊方法對調試自定義類也特別有用。

序號目的所編寫代碼Python 實際調用
初始化一個實例x = MyClass() x.__init__()
字符串的“官方”表現形式repr(x) x.__repr__()
字符串的“非正式”值str(x) x.__str__()
字節數組的“非正式”值bytes(x) x.__bytes__()
格式化字符串的值format(x, format_spec) x.__format__(format_spec)
  1. __init__() 方法的調用發生在實例被創建 之后 。如果要控制實際創建進程,請使用 __new__() 方法
  2. 按照約定, __repr__() 方法所返回的字符串為合法的 Python 表達式。
  3. 在調用 print(x) 的同時也調用了 __str__() 方法。
  4. 由于 bytes 類型的引入而從 Python 3 開始出現
  5. 按照約定,format_spec 應當遵循 迷你語言格式規范【Format Specification Mini-Language】。Python 標準類庫中的 decimal.py 提供了自己的 __format__() 方法。

行為方式與迭代器類似的類

《迭代器》一章中,我們已經學習了如何使用 __iter__()__next__() 方法從零開始創建迭代器。

序號目的所編寫代碼Python 實際調用
遍歷某個序列iter(seq) seq.__iter__()
從迭代器中獲取下一個值next(seq) seq.__next__()
按逆序創建一個迭代器reversed(seq) seq.__reversed__()
  1. 無論何時創建迭代器都將調用 __iter__() 方法。這是用初始值對迭代器進行初始化的絕佳之處。
  2. 無論何時從迭代器中獲取下一個值都將調用 __next__() 方法。
  3. __reversed__() 方法并不常用。它以一個現有序列為參數,并將該序列中所有元素從尾到頭以逆序排列生成一個新的迭代器。

正如我們在 《迭代器》一章中看到的,for 循環也可用作迭代器。在下面的循環中:

for x in seq:
    print(x)

Python 3 將會調用 seq.__iter__() 以創建一個迭代器,然后對迭代器調用 __next__() 方法以獲取 x 的每個值。當 __next__() 方法引發 StopIteration 例外時, for 循環正常結束。

計算屬性

序號目的所編寫代碼Python 實際調用
獲取一個計算屬性(無條件的)x.my_property x.__getattribute__('my_property')
獲取一個計算屬性(后備)x.my_property x.__getattr__('my_property')
設置某屬性x.my_property = value x.__setattr__('my_property', value)
刪除某屬性del x.my_property x.__delattr__('my_property')
列出所有屬性和方法dir(x) x.__dir__()
  1. 如果某個類定義了 __getattribute__() 方法,在 每次引用屬性或方法名稱時 Python 都調用它(特殊方法名稱除外,因為那樣將會導致討厭的無限循環)。
  2. 如果某個類定義了 __getattr__() 方法,Python 將只在正常的位置查詢屬性時才會調用它。如果實例 x 定義了屬性 colorx.color不會 調用 x.__getattr__('color');而只會返回 x.color 已定義好的值。
  3. 無論何時給屬性賦值,都會調用 __setattr__() 方法。
  4. 無論何時刪除一個屬性,都將調用 __delattr__() 方法。
  5. 如果定義了 __getattr__()__getattribute__() 方法, __dir__() 方法將非常有用。通常,調用 dir(x) 將只顯示正常的屬性和方法。如果 __getattr()__ 方法動態處理 color 屬性, dir(x) 將不會將 color 列為可用屬性。可通過覆蓋 __dir__() 方法允許將 color 列為可用屬性,對于想使用你的類但卻不想深入其內部的人來說,該方法非常有益。

__getattr__()__getattribute__() 方法的區別非常細微,但非常重要。可以用兩個例子來解釋一下:

class Dynamo:
    def __getattr__(self, key):
        if key == 'color':         
            return 'PapayaWhip'
        else:
            raise AttributeError   

>>> dyn = Dynamo()
>>> dyn.color                      
'PapayaWhip'
>>> dyn.color = 'LemonChiffon'
>>> dyn.color                      
'LemonChiffon'
  1. 屬性名稱以字符串的形式傳入 __getattr()__ 方法。如果名稱為 'color',該方法返回一個值。(在此情況下,它只是一個硬編碼的字符串,但可以正常地進行某些計算并返回結果。)
  2. 如果屬性名稱未知, __getattr()__ 方法必須引發一個 AttributeError 例外,否則在訪問未定義屬性時,代碼將只會默默地失敗。(從技術角度而言,如果方法不引發例外或顯式地返回一個值,它將返回 None ——Python 的空值。這意味著 所有 未顯式定義的屬性將為 None,幾乎可以肯定這不是你想看到的。)
  3. dyn 實例沒有名為 color 的屬性,因此在提供計算值時將調用 __getattr__()
  4. 在顯式地設置 dyn.color 之后,將不再為提供 dyn.color 的值而調用 __getattr__() 方法,因為 dyn.color 已在該實例中定義。

另一方面,__getattribute__() 方法是絕對的、無條件的。

class SuperDynamo:
    def __getattribute__(self, key):
        if key == 'color':
            return 'PapayaWhip'
        else:
            raise AttributeError

>>> dyn = SuperDynamo()
>>> dyn.color                      
'PapayaWhip'
>>> dyn.color = 'LemonChiffon'
>>> dyn.color                      
'PapayaWhip'
  1. 在獲取 dyn.color 的值時將調用 __getattribute__() 方法。
  2. 即便已經顯式地設置 dyn.color,在獲取 dyn.color 的值時, 仍將調用 __getattribute__() 方法。如果存在 __getattribute__() 方法,將在每次查找屬性和方法時 無條件地調用 它,哪怕在創建實例之后已經顯式地設置了屬性。

? 如果定義了類的 __getattribute__() 方法,你可能還想定義一個 __setattr__() 方法,并在兩者之間進行協同,以跟蹤屬性的值。否則,在創建實例之后所設置的值將會消失在黑洞中。

必須特別小心 __getattribute__() 方法,因為 Python 在查找類的方法名稱時也將對其進行調用。

class Rastan:
    def __getattribute__(self, key):
        raise AttributeError           
    def swim(self):
        pass

>>> hero = Rastan()
>>> hero.swim()                        
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __getattribute__
AttributeError
  1. 該類定義了一個總是引發 AttributeError 例外的 __getattribute__() 方法。沒有屬性或方法的查詢會成功。
  2. 調用 hero.swim() 時,Python 將在 Rastan 類中查找 swim() 方法。該查找將執行整個 __getattribute__() 方法,因為所有的屬性和方法查找都通過 __getattribute__() 方法。在此例中, __getattribute__() 方法引發 AttributeError 例外,因此該方法查找過程將會失敗,而方法調用也將失敗。

行為方式與函數類似的類

可以讓類的實例變得可調用——就像函數可以調用一樣——通過定義 __call__() 方法。

序號目的所編寫代碼Python 實際調用
像調用函數一樣“調用”一個實例my_instance() my_instance.__call__()

zipfile 模塊 通過該方式定義了一個可以使用給定密碼解密 經加密 zip 文件的類。該 zip 解密 算法需要在解密的過程中保存狀態。通過將解密器定義為類,使我們得以在 decryptor 類的單個實例中對該狀態進行維護。狀態在 __init__() 方法中進行初始化,如果文件 經加密 則進行更新。但由于該類像函數一樣“可調用”,因此可以將實例作為 map() 函數的第一個參數傳入,代碼如下:

# excerpt from zipfile.py
class _ZipDecrypter:
.
.
.
    def __init__(self, pwd):
        self.key0 = 305419896               
        self.key1 = 591751049
        self.key2 = 878082192
        for p in pwd:
            self._UpdateKeys(p)

    def __call__(self, c):                  
        assert isinstance(c, int)
        k = self.key2 | 2
        c = c ^ (((k * (k^1)) >> 8) & 255)
        self._UpdateKeys(c)
        return c
.
.
.
zd = _ZipDecrypter(pwd)                    
bytes = zef_file.read(12)
h = list(map(zd, bytes[0:12]))             
  1. _ZipDecryptor 類維護了以三個旋轉密鑰形式出現的狀態,該狀態稍后將在 _UpdateKeys() 方法中更新(此處未展示)。
  2. 該類定義了一個 __call__() 方法,使得該類可像函數一樣調用。在此例中,__call__() 對 zip 文件的單個字節進行解密,然后基于經解密的字節對旋轉密碼進行更新。
  3. zd_ZipDecryptor 類的一個實例。變量 pwd 被傳入 __init__() 方法,并在其中被存儲和用于首次旋轉密碼更新。
  4. 給出 zip 文件的頭 12 個字節,將這些字節映射給 zd 進行解密,實際上這將導致調用 __call__() 方法 12 次,也就是 更新內部狀態并返回結果字節 12 次。

行為方式與序列類似的類

如果類作為一系列值的容器出現——也就是說如果對某個類來說,是否“包含”某值是件有意義的事情——那么它也許應該定義下面的特殊方法已,讓它的行為方式與序列類似。

序號目的所編寫代碼Python 實際調用
序列的長度len(seq) seq.__len__()
了解某序列是否包含特定的值x in seq seq.__contains__(x)

cgi 模塊 在其 FieldStorage 類中使用了這些方法,該類用于表示提交給動態網頁的所有表單字段或查詢參數。

# A script which responds to http://example.com/search?q=cgi
import cgi
fs = cgi.FieldStorage()
if 'q' in fs:                                               
  do_search()

# An excerpt from cgi.py that explains how that works
class FieldStorage:
.
.
.
    def __contains__(self, key):                            
        if self.list is None:
            raise TypeError('not indexable')
        return any(item.name == key for item in self.list)  

    def __len__(self):                                      
        return len(self.keys())                             
  1. 一旦創建了 cgi.FieldStorage 類的實例,就可以使用 “in” 運算符來檢查查詢字符串中是否包含了某個特定參數。
  2. __contains__() 方法是令該魔法生效的主角。
  3. 如果代碼為 if 'q' in fs,Python 將在 fs 對象中查找 __contains__() 方法,而該方法在 cgi.py 中已經定義。'q' 的值被當作 key 參數傳入 __contains__() 方法。
  4. 同樣的 FieldStorage 類還支持返回其長度,因此可以編寫代碼 len(fs) 而其將調用 FieldStorage__len__() 方法,并返回其識別的查詢參數個數。
  5. self.keys() 方法檢查 self.list is None 是否為真值,因此 __len__ 方法無需重復該錯誤檢查。

行為方式與字典類似的類

在前一節的基礎上稍作拓展,就不僅可以對 “in” 運算符和 len() 函數進行響應,還可像全功能字典一樣根據鍵來返回值。

序號目的所編寫代碼Python 實際調用
通過鍵來獲取值x[key] x.__getitem__(key)
通過鍵來設置值x[key] = value x.__setitem__(key, value)
刪除一個鍵值對del x[key] x.__delitem__(key)
為缺失鍵提供默認值x[nonexistent_key] x.__missing__(nonexistent_key)

cgi 模塊FieldStorage 同樣定義了這些特殊方法,也就是說可以像下面這樣編碼:

# A script which responds to http://example.com/search?q=cgi
import cgi
fs = cgi.FieldStorage()
if 'q' in fs:
  do_search(fs['q'])                              

# An excerpt from cgi.py that shows how it works
class FieldStorage:
.
.
.
    def __getitem__(self, key):                   
        if self.list is None:
            raise TypeError('not indexable')
        found = []
        for item in self.list:
            if item.name == key: found.append(item)
        if not found:
            raise KeyError(key)
        if len(found) == 1:
            return found[0]
        else:
            return found
  1. fs 對象是 cgi.FieldStorage 類的一個實例,但仍然可以像 fs['q'] 這樣估算表達式。
  2. fs['q']key 參數設置為 'q' 來調用 __getitem__() 方法。然后它將在其內部維護的查詢參數列表 (self.list) 中查找一個 .name 與給定鍵相符的字典項。

行為方式與數值類似的類

使用適當的特殊方法,可以將類的行為方式定義為與數字相仿。也就是說,可以進行相加、相減,并進行其它數學運算。這就是 分數 的實現方式—— Fraction 類實現了這些特殊方法,然后就可以進行下列運算了:

>>> from fractions import Fraction
>>> x = Fraction(1, 3)
>>> x / 3
Fraction(1, 9)

以下是實現“類數字”類的完整特殊方法清單:

序號目的所編寫代碼Python 實際調用
加法x + y x.__add__(y)
減法x - y x.__sub__(y)
乘法x * y x.__mul__(y)
除法x / y x.__truediv__(y)
地板除x // y x.__floordiv__(y)
取模(取余)x % y x.__mod__(y)
地板除 & 取模divmod(x, y) x.__divmod__(y)
乘冪x ** y x.__pow__(y)
左位移x << y x.__lshift__(y)
右位移x >> y x.__rshift__(y)
按位 and x & y x.__and__(y)
按位 xor x ^ y x.__xor__(y)
按位 or x | y x.__or__(y)

如果 x 是某個實現了所有這些方法的類的實例,那么萬事大吉。但如果未實現其中之一呢?或者更糟,如果實現了,但卻無法處理某幾類參數會怎么樣?例如:

>>> from fractions import Fraction
>>> x = Fraction(1, 3)
>>> 1 / x
Fraction(3, 1)

這并 不是 傳入一個 分數 并將其除以一個整數(如前例那樣)的情況。前例中的情況非常直觀: x / 3 調用 x.__truediv__(3),而Fraction__truediv__() 方法處理所有的數學運算。但整數并不“知道”如何對分數進行數學計算。因此本例該如何運作呢?

反映操作 相關的還有第二部分算數特殊方法。給定一個二元算術運算 (例如: x / y),有兩種方法來實現它:

  1. 告訴 x 將自己除以 y,或者
  2. 告訴 y 去除 x

之前提到的特殊方法集合采用了第一種方式:對于給定 x / y,它們為 x 提供了一種途徑來表述“我知道如何將自己除以 y。”下面的特殊方法集合采用了第二種方法:它們向 y 提供了一種途徑來表述“我知道如何成為分母,并用自己去除 x。”

序號目的所編寫代碼Python 實際調用
加法x + y y.__radd__(x)
減法x - y y.__rsub__(x)
乘法x * y y.__rmul__(x)
除法x / y y.__rtruediv__(x)
地板除x // y y.__rfloordiv__(x)
取模(取余)x % y y.__rmod__(x)
地板除 & 取模divmod(x, y) y.__rdivmod__(x)
乘冪x ** y y.__rpow__(x)
左位移x << y y.__rlshift__(x)
右位移x >> y y.__rrshift__(x)
按位 and x & y y.__rand__(x)
按位 xor x ^ y y.__rxor__(x)
按位 or x | y y.__ror__(x)

但是等一下!還有更多特殊方法!如果在進行“原地”操作,如: x /= 3,還可定義更多的特殊方法。

序號目的所編寫代碼Python 實際調用
原地加法x += y x.__iadd__(y)
原地減法x -= y x.__isub__(y)
原地乘法x *= y x.__imul__(y)
原地除法x /= y x.__itruediv__(y)
原地地板除法x //= y x.__ifloordiv__(y)
原地取模x %= y x.__imod__(y)
原地乘冪x **= y x.__ipow__(y)
原地左位移x <<= y x.__ilshift__(y)
原地右位移x >>= y x.__irshift__(y)
原地按位 and x &= y x.__iand__(y)
原地按位 xor x ^= y x.__ixor__(y)
原地按位 or x |= y x.__ior__(y)

注意:多數情況下,并不需要原地操作方法。如果未對特定運算定義“就地”方法,Python 將會試著使用(普通)方法。例如,為執行表達式 x /= y,Python 將會:

  1. 試著調用 x.__itruediv__(y)。如果該方法已經定義,并返回了 NotImplemented 之外的值,那已經大功告成了。
  2. 試圖調用 x.__truediv__(y)。如果該方法已定義并返回一個 NotImplemented 之外的值, x 的舊值將被丟棄,并將所返回的值替代它,就像是進行了 x = x / y 運算。
  3. 試圖調用 y.__rtruediv__(x)。如果該方法已定義并返回了一個 NotImplemented 之外的值,x 的舊值將被丟棄,并用所返回值進行替換。

因此如果想對原地運算進行優化,僅需像 __itruediv__() 方法一樣定義“原地”方法。否則,基本上 Python 將會重新生成原地運算公式,以使用常規的運算及變量賦值。

還有一些“一元”數學運算,可以對“類-數字”對象自己執行。

序號目的所編寫代碼Python 實際調用
負數-x x.__neg__()
正數+x x.__pos__()
絕對值abs(x) x.__abs__()
取反~x x.__invert__()
復數complex(x) x.__complex__()
整數轉換int(x) x.__int__()
浮點數float(x) x.__float__()
四舍五入至最近的整數round(x) x.__round__()
四舍五入至最近的 n 位小數round(x, n) x.__round__(n)
>= x 的最小整數 math.ceil(x) x.__ceil__()
<= x的最大整數 math.floor(x) x.__floor__()
x 朝向 0 取整math.trunc(x) x.__trunc__()
PEP 357 作為列表索引的數字a_list[x] a_list[x.__index__()]

可比較的類

我將此內容從前一節中拿出來使其單獨成節,是因為“比較”操作并不局限于數字。許多數據類型都可以進行比較——字符串、列表,甚至字典。如果要創建自己的類,且對象之間的比較有意義,可以使用下面的特殊方法來實現比較。

序號目的所編寫代碼Python 實際調用
相等x == y x.__eq__(y)
不相等x != y x.__ne__(y)
小于x < y x.__lt__(y)
小于或等于x <= y x.__le__(y)
大于x > y x.__gt__(y)
大于或等于x >= y x.__ge__(y)
布爾上上下文環境中的真值if x: x.__bool__()

?如果定義了 __lt__() 方法但沒有定義 __gt__() 方法,Python 將通過經交換的算子調用 __lt__() 方法。然而,Python 并不會組合方法。例如,如果定義了 __lt__() 方法和 __eq()__ 方法,并試圖測試是否 x <= y,Python 不會按順序調用 __lt__()__eq()__ 。它將只調用 __le__() 方法。

可序列化的類

Python 支持 任意對象的序列化和反序列化。(多數 Python 參考資料稱該過程為 “pickling” 和 “unpickling”)。該技術對與將狀態保存為文件并在稍后恢復它非常有意義。所有的 內置數據類型 均已支持 pickling 。如果創建了自定義類,且希望它能夠 pickle,閱讀 pickle 協議 了解下列特殊方法何時以及如何被調用。

序號目的所編寫代碼Python 實際調用
自定義對象的復制copy.copy(x) x.__copy__()
自定義對象的深度復制copy.deepcopy(x) x.__deepcopy__()
在 pickling 之前獲取對象的狀態pickle.dump(x, file) x.__getstate__()
序列化某對象pickle.dump(x, file) x.__reduce__()
序列化某對象(新 pickling 協議)pickle.dump(x, file, protocol_version) x.__reduce_ex__(protocol_version)
* 控制 unpickling 過程中對象的創建方式x = pickle.load(file) x.__getnewargs__()
* 在 unpickling 之后還原對象的狀態x = pickle.load(file) x.__setstate__()

* 要重建序列化對象,Python 需要創建一個和被序列化的對象看起來一樣的新對象,然后設置新對象的所有屬性。__getnewargs__() 方法控制新對象的創建過程,而 __setstate__() 方法控制屬性值的還原方式。

可在 with 語塊中使用的類

with 語塊定義了 運行時刻上下文環境;在執行 with 語句時將“進入”該上下文環境,而執行該語塊中的最后一條語句將“退出”該上下文環境。

序號目的所編寫代碼Python 實際調用
在進入 with 語塊時進行一些特別操作with x: x.__enter__()
在退出 with 語塊時進行一些特別操作with x: x.__exit__()

以下是 with file 習慣用法 的運作方式:

# excerpt from io.py:
def _checkClosed(self, msg=None):
    '''Internal: raise an ValueError if file is closed
    '''
    if self.closed:
        raise ValueError('I/O operation on closed file.'
                         if msg is None else msg)

def __enter__(self):
    '''Context management protocol.  Returns self.'''
    self._checkClosed()                                
    return self                                        

def __exit__(self, *args):
    '''Context management protocol.  Calls close()'''
    self.close()                                       
  1. 該文件對象同時定義了一個 __enter__() 和一個 __exit__() 方法。該 __enter__() 方法檢查文件是否處于打開狀態;如果沒有, _checkClosed() 方法引發一個例外。
  2. __enter__() 方法將始終返回 self —— 這是 with 語塊將用于調用屬性和方法的對象
  3. with 語塊結束后,文件對象將自動關閉。怎么做到的?在 __exit__() 方法中調用了 self.close() .

?__exit__() 方法將總是被調用,哪怕是在 with 語塊中引發了例外。實際上,如果引發了例外,該例外信息將會被傳遞給 __exit__() 方法。查閱 With 狀態上下文環境管理器 了解更多細節。

要了解關于上下文管理器的更多內容,請查閱 《自動關閉文件》《重定向標準輸出》

真正神奇的東西

如果知道自己在干什么,你幾乎可以完全控制類是如何比較的、屬性如何定義,以及類的子類是何種類型。

序號目的所編寫代碼Python 實際調用
類構造器x = MyClass() x.__new__()
* 類析構器del x x.__del__()
只定義特定集合的某些屬性 x.__slots__()
自定義散列值hash(x) x.__hash__()
獲取某個屬性的值x.color type(x).__dict__['color'].__get__(x, type(x))
設置某個屬性的值x.color = 'PapayaWhip' type(x).__dict__['color'].__set__(x, 'PapayaWhip')
刪除某個屬性del x.color type(x).__dict__['color'].__del__(x)
控制某個對象是否是該對象的實例 your classisinstance(x, MyClass) MyClass.__instancecheck__(x)
控制某個類是否是該類的子類issubclass(C, MyClass) MyClass.__subclasscheck__(C)
控制某個類是否是該抽象基類的子類issubclass(C, MyABC) MyABC.__subclasshook__(C)

* 確切掌握 Python 何時調用 __del__() 特別方法 是件難以置信的復雜事情。要想完全理解它,必須清楚 Python 如何在內存中跟蹤對象。以下有一篇好文章介紹 Python 垃圾收集和類析構器。還可以閱讀 《弱引用》weakref 模塊》,還可以將 gc 模塊》 當作補充閱讀材料。

深入閱讀

本附錄中提到的模塊:

其它啟發式閱讀:

? 2001–9 Mark Pilgrim

<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            亚洲欧美在线