當前位置: 首頁 ? 深入 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)
|
__init__() 方法的調用發生在實例被創建 之后 。如果要控制實際創建進程,請使用 __new__() 方法。__repr__() 方法所返回的字符串為合法的 Python 表達式。print(x) 的同時也調用了 __str__() 方法。bytes 類型的引入而從 Python 3 開始出現。decimal.py 提供了自己的 __format__() 方法。在 《迭代器》一章中,我們已經學習了如何使用 __iter__() 和 __next__() 方法從零開始創建迭代器。
| 序號 | 目的 | 所編寫代碼 | Python 實際調用 |
|---|---|---|---|
| ① | 遍歷某個序列 | iter(seq)
| seq.__iter__()
|
| ② | 從迭代器中獲取下一個值 | next(seq)
| seq.__next__()
|
| ③ | 按逆序創建一個迭代器 | reversed(seq)
| seq.__reversed__()
|
__iter__() 方法。這是用初始值對迭代器進行初始化的絕佳之處。__next__() 方法。__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__()
|
__getattribute__() 方法,在 每次引用屬性或方法名稱時 Python 都調用它(特殊方法名稱除外,因為那樣將會導致討厭的無限循環)。__getattr__() 方法,Python 將只在正常的位置查詢屬性時才會調用它。如果實例 x 定義了屬性 color, x.color 將 不會 調用 x.__getattr__('color');而只會返回 x.color 已定義好的值。__setattr__() 方法。__delattr__() 方法。__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'
__getattr()__ 方法。如果名稱為 'color',該方法返回一個值。(在此情況下,它只是一個硬編碼的字符串,但可以正常地進行某些計算并返回結果。)__getattr()__ 方法必須引發一個 AttributeError 例外,否則在訪問未定義屬性時,代碼將只會默默地失敗。(從技術角度而言,如果方法不引發例外或顯式地返回一個值,它將返回 None ——Python 的空值。這意味著 所有 未顯式定義的屬性將為 None,幾乎可以肯定這不是你想看到的。)__getattr__() 。__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'
__getattribute__() 方法。__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
AttributeError 例外的 __getattribute__() 方法。沒有屬性或方法的查詢會成功。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])) ④
_ZipDecryptor 類維護了以三個旋轉密鑰形式出現的狀態,該狀態稍后將在 _UpdateKeys() 方法中更新(此處未展示)。__call__() 方法,使得該類可像函數一樣調用。在此例中,__call__() 對 zip 文件的單個字節進行解密,然后基于經解密的字節對旋轉密碼進行更新。_ZipDecryptor 類的一個實例。變量 pwd 被傳入 __init__() 方法,并在其中被存儲和用于首次旋轉密碼更新。__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()) ⑤
cgi.FieldStorage 類的實例,就可以使用 “in” 運算符來檢查查詢字符串中是否包含了某個特定參數。__contains__() 方法是令該魔法生效的主角。if 'q' in fs,Python 將在 fs 對象中查找 __contains__() 方法,而該方法在 cgi.py 中已經定義。'q' 的值被當作 key 參數傳入 __contains__() 方法。FieldStorage 類還支持返回其長度,因此可以編寫代碼 len(fs) 而其將調用 FieldStorage 的 __len__() 方法,并返回其識別的查詢參數個數。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
cgi.FieldStorage 類的一個實例,但仍然可以像 fs['q'] 這樣估算表達式。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),有兩種方法來實現它:
之前提到的特殊方法集合采用了第一種方式:對于給定 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 將會:
x.__itruediv__(y)。如果該方法已經定義,并返回了 NotImplemented 之外的值,那已經大功告成了。x.__truediv__(y)。如果該方法已定義并返回一個 NotImplemented 之外的值, x 的舊值將被丟棄,并將所返回的值替代它,就像是進行了 x = x / y 運算。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() ③
__enter__() 和一個 __exit__() 方法。該 __enter__() 方法檢查文件是否處于打開狀態;如果沒有, _checkClosed() 方法引發一個例外。__enter__() 方法將始終返回 self —— 這是 with 語塊將用于調用屬性和方法的對象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 class | isinstance(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