本章主要關(guān)注點的是和類定義有關(guān)的常見編程模型。包括讓對象支持常見的 Python 特性、特殊方法的使用、 類封裝技術(shù)、繼承、內(nèi)存管理以及有用的設(shè)計模式。
你想改變對象實例的打印或顯示輸出,讓它們更具可讀性。
要改變一個實例的字符串表示,可重新定義它的 __str__()和 __repr__() 方法。例如:
class Pair:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return 'Pair({0.x!r}, {0.y!r})'.format(self)
def __str__(self):
return '({0.x!s}, {0.y!s})'.format(self)
__repr__()方法返回一個實例的代碼表示形式,通常用來重新構(gòu)造這個實例。 內(nèi)置的 repr()函數(shù)返回這個字符串,跟我們使用交互式解釋器顯示的值是一樣的。 __str__()方法將實例轉(zhuǎn)換為一個字符串,使用 str() 或 print() 函數(shù)會輸出這個字符串。比如:
>>> p = Pair(3, 4)
>>> p
Pair(3, 4) # __repr__() output
>>> print(p)
(3, 4) # __str__() output
>>>
我們在這里還演示了在格式化的時候怎樣使用不同的字符串表現(xiàn)形式。 特別來講,!r 格式化代碼指明輸出使用__repr__() 來代替默認的__str__() 。 你可以用前面的類來試著測試下:
>>> p = Pair(3, 4)
>>> print('p is {0!r}'.format(p))
p is Pair(3, 4)
>>> print('p is {0}'.format(p))
p is (3, 4)
>>>
自定義 __repr__()和__str__() 通常是很好的習(xí)慣,因為它能簡化調(diào)試和實例輸出。 例如,如果僅僅只是打印輸出或日志輸出某個實例,那么程序員會看到實例更加詳細與有用的信息。
__repr__() 生成的文本字符串標(biāo)準(zhǔn)做法是需要讓 eval(repr(x)) == x為真。 如果實在不能這樣子做,應(yīng)該創(chuàng)建一個有用的文本表示,并使用 < 和 > 括起來。比如:
>>> f = open('file.dat')
>>> f
<_io.TextIOWrapper name='file.dat' mode='r' encoding='UTF-8'>
>>>
如果 __str__() 沒有被定義,那么就會使用 __repr__() 來代替輸出。
上面的 format()方法的使用看上去很有趣,格式化代碼 {0.x}對應(yīng)的是第1個參數(shù)的 x 屬性。 因此,在下面的函數(shù)中,0實際上指的就是 self本身:
def __repr__(self):
return 'Pair({0.x!r}, {0.y!r})'.format(self)
作為這種實現(xiàn)的一個替代,你也可以使用 % 操作符,就像下面這樣:
def __repr__(self):
return 'Pair(%r, %r)' % (self.x, self.y)
你想通過 format()函數(shù)和字符串方法使得一個對象能支持自定義的格式化。
為了自定義字符串的格式化,我們需要在類上面定義__format__() 方法。例如:
_formats = {
'ymd' : '{d.year}-{d.month}-{d.day}',
'mdy' : '{d.month}/{d.day}/{d.year}',
'dmy' : '{d.day}/{d.month}/{d.year}'
}
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __format__(self, code):
if code == '':
code = 'ymd'
fmt = _formats[code]
return fmt.format(d=self)
現(xiàn)在Date類的實例可以支持格式化操作了,如同下面這樣:
>>> d = Date(2012, 12, 21)
>>> format(d)
'2012-12-21'
>>> format(d, 'mdy')
'12/21/2012'
>>> 'The date is {:ymd}'.format(d)
'The date is 2012-12-21'
>>> 'The date is {:mdy}'.format(d)
'The date is 12/21/2012'
>>>
__format__()方法給 Python 的字符串格式化功能提供了一個鉤子。 這里需要著重強調(diào)的是格式化代碼的解析工作完全由類自己決定。因此,格式化代碼可以是任何值。 例如,參考下面來自 datetime 模塊中的代碼:
>>> from datetime import date
>>> d = date(2012, 12, 21)
>>> format(d)
'2012-12-21'
>>> format(d,'%A, %B %d, %Y')
'Friday, December 21, 2012'
>>> 'The end is {:%d %b %Y}. Goodbye'.format(d)
'The end is 21 Dec 2012. Goodbye'
>>>
對于內(nèi)置類型的格式化有一些標(biāo)準(zhǔn)的約定。 可以參考 string 模塊文檔說明。
你想讓你的對象支持上下文管理協(xié)議(with 語句)。
為了讓一個對象兼容 with語句,你需要實現(xiàn) __enter__() 和 __exit__() 方法。 例如,考慮如下的一個類,它能為我們創(chuàng)建一個網(wǎng)絡(luò)連接:
from socket import socket, AF_INET, SOCK_STREAM
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = family
self.type = type
self.sock = None
def __enter__(self):
if self.sock is not None:
raise RuntimeError('Already connected')
self.sock = socket(self.family, self.type)
self.sock.connect(self.address)
return self.sock
def __exit__(self, exc_ty, exc_val, tb):
self.sock.close()
self.sock = None
這個類的關(guān)鍵特點在于它表示了一個網(wǎng)絡(luò)連接,但是初始化的時候并不會做任何事情(比如它并沒有建立一個連接)。 連接的建立和關(guān)閉是使用 with語句自動完成的,例如:
from functools import partial
conn = LazyConnection(('www.python.org', 80))
# Connection closed
with conn as s:
# conn.__enter__() executes: connection open
s.send(b'GET /index.html HTTP/1.0\r\n')
s.send(b'Host: www.python.org\r\n')
s.send(b'\r\n')
resp = b''.join(iter(partial(s.recv, 8192), b''))
# conn.__exit__() executes: connection closed
編寫上下文管理器的主要原理是你的代碼會放到with語句塊中執(zhí)行。 當(dāng)出現(xiàn) with 語句的時候,對象的 __enter__()方法被觸發(fā), 它返回的值(如果有的話)會被賦值給 as 聲明的變量。然后,with 語句塊里面的代碼開始執(zhí)行。 最后,__exit__() 方法被觸發(fā)進行清理工作。
不管 with代碼塊中發(fā)生什么,上面的控制流都會執(zhí)行完,就算代碼塊中發(fā)生了異常也是一樣的。 事實上,__exit__() 方法的第三個參數(shù)包含了異常類型、異常值和追溯信息(如果有的話)。 __exit__() 方法能自己決定怎樣利用這個異常信息,或者忽略它并返回一個 None 值。 如果 __exit__() 返回 True ,那么異常會被清空,就好像什么都沒發(fā)生一樣, with語句后面的程序繼續(xù)在正常執(zhí)行。
還有一個細節(jié)問題就是 LazyConnection 類是否允許多個 with 語句來嵌套使用連接。 很顯然,上面的定義中一次只能允許一個 socket 連接,如果正在使用一個 socket 的時候又重復(fù)使用 with 語句, 就會產(chǎn)生一個異常了。不過你可以像下面這樣修改下上面的實現(xiàn)來解決這個問題:
from socket import socket, AF_INET, SOCK_STREAM
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = family
self.type = type
self.connections = []
def __enter__(self):
sock = socket(self.family, self.type)
sock.connect(self.address)
self.connections.append(sock)
return sock
def __exit__(self, exc_ty, exc_val, tb):
self.connections.pop().close()
# Example use
from functools import partial
conn = LazyConnection(('www.python.org', 80))
with conn as s1:
pass
with conn as s2:
pass
# s1 and s2 are independent sockets
在第二個版本中,LazyConnection 類可以被看做是某個連接工廠。在內(nèi)部,一個列表被用來構(gòu)造一個棧。 每次 __enter__() 方法執(zhí)行的時候,它復(fù)制創(chuàng)建一個新的連接并將其加入到棧里面。 __exit__() 方法簡單的從棧中彈出最后一個連接并關(guān)閉它。 這里稍微有點難理解,不過它能允許嵌套使用 with 語句創(chuàng)建多個連接,就如上面演示的那樣。
在需要管理一些資源比如文件、網(wǎng)絡(luò)連接和鎖的編程環(huán)境中,使用上下文管理器是很普遍的。 這些資源的一個主要特征是它們必須被手動的關(guān)閉或釋放來確保程序的正確運行。 例如,如果你請求了一個鎖,那么你必須確保之后釋放了它,否則就可能產(chǎn)生死鎖。 通過實現(xiàn)__enter__()和__exit__() 方法并使用with 語句可以很容易的避免這些問題, 因為 __exit__()方法可以讓你無需擔(dān)心這些了。
在contextmanager模塊中有一個標(biāo)準(zhǔn)的上下文管理方案模板,可參考9.22小節(jié)。 同時在12.6小節(jié)中還有一個對本節(jié)示例程序的線程安全的修改版。
你的程序要創(chuàng)建大量(可能上百萬)的對象,導(dǎo)致占用很大的內(nèi)存。
對于主要是用來當(dāng)成簡單的數(shù)據(jù)結(jié)構(gòu)的類而言,你可以通過給類添加 __slots__ 屬性來極大的減少實例所占的內(nèi)存。比如:
class Date:
__slots__ = ['year', 'month', 'day']
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
當(dāng)你定義 __slots__ 后,Python 就會為實例使用一種更加緊湊的內(nèi)部表示。 實例通過一個很小的固定大小的數(shù)組來構(gòu)建,而不是為每個實例定義一個字典,這跟元組或列表很類似。 在__slots__ 中列出的屬性名在內(nèi)部被映射到這個數(shù)組的指定小標(biāo)上。 使用 slots 一個不好的地方就是我們不能再給實例添加新的屬性了,只能使用在 __slots__中定義的那些屬性名。
使用 slots 后節(jié)省的內(nèi)存會跟存儲屬性的數(shù)量和類型有關(guān)。 不過,一般來講,使用到的內(nèi)存總量和將數(shù)據(jù)存儲在一個元組中差不多。 為了給你一個直觀認識,假設(shè)你不使用 slots 直接存儲一個 Date 實例, 在64位的 Python 上面要占用428字節(jié),而如果使用了 slots,內(nèi)存占用下降到156字節(jié)。 如果程序中需要同時創(chuàng)建大量的日期實例,那么這個就能極大的減小內(nèi)存使用量了。
盡管 slots 看上去是一個很有用的特性,很多時候你還是得減少對它的使用沖動。 Python 的很多特性都依賴于普通的基于字典的實現(xiàn)。 另外,定義了 slots 后的類不再支持一些普通類特性了,比如多繼承。 大多數(shù)情況下,你應(yīng)該只在那些經(jīng)常被使用到的用作數(shù)據(jù)結(jié)構(gòu)的類上定義 slots (比如在程序中需要創(chuàng)建某個類的幾百萬個實例對象)。
關(guān)于 __slots__ 的一個常見誤區(qū)是它可以作為一個封裝工具來防止用戶給實例增加新的屬性。 盡管使用 slots 可以達到這樣的目的,但是這個并不是它的初衷。 __slots__ 更多的是用來作為一個內(nèi)存優(yōu)化工具。
你想封裝類的實例上面的“私有”數(shù)據(jù),但是 Python 語言并沒有訪問控制。
Python 程序員不去依賴語言特性去封裝數(shù)據(jù),而是通過遵循一定的屬性和方法命名規(guī)約來達到這個效果。 第一個約定是任何以單下劃線_開頭的名字都應(yīng)該是內(nèi)部實現(xiàn)。比如:
class A:
def __init__(self):
self._internal = 0 # An internal attribute
self.public = 1 # A public attribute
def public_method(self):
'''
A public method
'''
pass
def _internal_method(self):
pass
Python 并不會真的阻止別人訪問內(nèi)部名稱。但是如果你這么做肯定是不好的,可能會導(dǎo)致脆弱的代碼。 同時還要注意到,使用下劃線開頭的約定同樣適用于模塊名和模塊級別函數(shù)。 例如,如果你看到某個模塊名以單下劃線開頭(比如_socket),那它就是內(nèi)部實現(xiàn)。 類似的,模塊級別函數(shù)比如 sys._getframe()在使用的時候就得加倍小心了。
你還可能會遇到在類定義中使用兩個下劃線(__)開頭的命名。比如:
class B:
def __init__(self):
self.__private = 0
def __private_method(self):
pass
def public_method(self):
pass
self.__private_method()
使用雙下劃線開始會導(dǎo)致訪問名稱變成其他形式。 比如,在前面的類B中,私有屬性會被分別重命名為_B__private和 _B__private_method。 這時候你可能會問這樣重命名的目的是什么,答案就是繼承——這種屬性通過繼承是無法被覆蓋的。比如:
class C(B):
def __init__(self):
super().__init__()
self.__private = 1 # Does not override B.__private
# Does not override B.__private_method()
def __private_method(self):
pass
這里,私有名稱__private 和__private_method被重命名為 _C__private和 _C__private_method ,這個跟父類 B 中的名稱是完全不同的。
上面提到有兩種不同的編碼約定(單下劃線和雙下劃線)來命名私有屬性,那么問題就來了:到底哪種方式好呢? 大多數(shù)而言,你應(yīng)該讓你的非公共名稱以單下劃線開頭。但是,如果你清楚你的代碼會涉及到子類, 并且有些內(nèi)部屬性應(yīng)該在子類中隱藏起來,那么才考慮使用雙下劃線方案。
還有一點要注意的是,有時候你定義的一個變量和某個保留關(guān)鍵字沖突,這時候可以使用單下劃線作為后綴,例如:
lambda_ = 2.0 # Trailing _ to avoid clash with lambda keyword
這里我們并不使用單下劃線前綴的原因是它避免誤解它的使用初衷 (如使用單下劃線前綴的目的是為了防止命名沖突而不是指明這個屬性是私有的)。 通過使用單下劃線后綴可以解決這個問題。
你想給某個實例 attribute 增加除訪問與修改之外的其他處理邏輯,比如類型檢查或合法性驗證。
自定義某個屬性的一種簡單方法是將它定義為一個 property。 例如,下面的代碼定義了一個 property,增加對一個屬性簡單的類型檢查:
class Person:
def __init__(self, first_name):
self.first_name = first_name
# Getter function
@property
def first_name(self):
return self._first_name
# Setter function
@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value
# Deleter function (optional)
@first_name.deleter
def first_name(self):
raise AttributeError("Can't delete attribute")
上述代碼中有三個相關(guān)聯(lián)的方法,這三個方法的名字都必須一樣。 第一個方法是一個 getter 函數(shù),它使得 first_name 成為一個屬性。 其他兩個方法給 first_name 屬性添加了 setter 和deleter函數(shù)。 需要強調(diào)的是只有在 first_name屬性被創(chuàng)建后, 后面的兩個裝飾器@first_name.setter 和 @first_name.deleter才能被定義。
property 的一個關(guān)鍵特征是它看上去跟普通的 attribute 沒什么兩樣, 但是訪問它的時候會自動觸發(fā) getter、setter 和 deleter 方法。例如:
>>> a = Person('Guido')
>>> a.first_name # Calls the getter
'Guido'
>>> a.first_name = 42 # Calls the setter
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "prop.py", line 14, in first_name
raise TypeError('Expected a string')
TypeError: Expected a string
>>> del a.first_name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can`t delete attribute
>>>
在實現(xiàn)一個 property 的時候,底層數(shù)據(jù)(如果有的話)仍然需要存儲在某個地方。 因此,在 get 和 set 方法中,你會看到對 _first_name 屬性的操作,這也是實際數(shù)據(jù)保存的地方。 另外,你可能還會問為什么 __init__()方法中設(shè)置了 self.first_name而不是 self._first_name。 在這個例子中,我們創(chuàng)建一個 property 的目的就是在設(shè)置 attribute 的時候進行檢查。 因此,你可能想在初始化的時候也進行這種類型檢查。通過設(shè)置 self.first_name ,自動調(diào)用 setter方法, 這個方法里面會進行參數(shù)的檢查,否則就是直接訪問self._first_name了。
還能在已存在的 get 和 set 方法基礎(chǔ)上定義 property。例如:
class Person:
def __init__(self, first_name):
self.set_first_name(first_name)
# Getter function
def get_first_name(self):
return self._first_name
# Setter function
def set_first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value
# Deleter function (optional)
def del_first_name(self):
raise AttributeError("Can't delete attribute")
# Make a property from existing get/set methods
name = property(get_first_name, set_first_name, del_first_name)
一個 property 屬性其實就是一系列相關(guān)綁定方法的集合。如果你去查看擁有 property 的類, 就會發(fā)現(xiàn) property 本身的 fget、fset 和 fdel 屬性就是類里面的普通方法。比如:
>>> Person.first_name.fget
<function Person.first_name at 0x1006a60e0>
>>> Person.first_name.fset
<function Person.first_name at 0x1006a6170>
>>> Person.first_name.fdel
<function Person.first_name at 0x1006a62e0>
>>>
通常來講,你不會直接取調(diào)用 fget 或者 fset,它們會在訪問 property 的時候自動被觸發(fā)。
只有當(dāng)你確實需要對 attribute 執(zhí)行其他額外的操作的時候才應(yīng)該使用到 property。 有時候一些從其他編程語言(比如 Java)過來的程序員總認為所有訪問都應(yīng)該通過 getter 和 setter, 所以他們認為代碼應(yīng)該像下面這樣寫:
class Person:
def __init__(self, first_name):
self.first_name = first_name
@property
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
self._first_name = value
不要寫這種沒有做任何其他額外操作的 property。 首先,它會讓你的代碼變得很臃腫,并且還會迷惑閱讀者。 其次,它還會讓你的程序運行起來變慢很多。 最后,這樣的設(shè)計并沒有帶來任何的好處。 特別是當(dāng)你以后想給普通 attribute 訪問添加額外的處理邏輯的時候, 你可以將它變成一個 property 而無需改變原來的代碼。 因為訪問 attribute 的代碼還是保持原樣。
Properties 還是一種定義動態(tài)計算 attribute 的方法。 這種類型的 attributes 并不會被實際的存儲,而是在需要的時候計算出來。比如:
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return math.pi * self.radius ** 2
@property
def diameter(self):
return self.radius ** 2
@property
def perimeter(self):
return 2 * math.pi * self.radius
在這里,我們通過使用 properties,將所有的訪問接口形式統(tǒng)一起來, 對半徑、直徑、周長和面積的訪問都是通過屬性訪問,就跟訪問簡單的 attribute 是一樣的。 如果不這樣做的話,那么就要在代碼中混合使用簡單屬性訪問和方法調(diào)用。 下面是使用的實例:
>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area # Notice lack of ()
50.26548245743669
>>> c.perimeter # Notice lack of ()
25.132741228718345
>>>
盡管 properties 可以實現(xiàn)優(yōu)雅的編程接口,但有些時候你還是會想直接使用 getter 和 setter 函數(shù)。例如:
>>> p = Person('Guido')
>>> p.get_first_name()
'Guido'
>>> p.set_first_name('Larry')
>>>
這種情況的出現(xiàn)通常是因為 Python 代碼被集成到一個大型基礎(chǔ)平臺架構(gòu)或程序中。 例如,有可能是一個 Python 類準(zhǔn)備加入到一個基于遠程過程調(diào)用的大型分布式系統(tǒng)中。 這種情況下,直接使用 get/set 方法(普通方法調(diào)用)而不是 property 或許會更容易兼容。
最后一點,不要像下面這樣寫有大量重復(fù)代碼的 property 定義:
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value
# Repeated property code, but for a different name (bad!)
@property
def last_name(self):
return self._last_name
@last_name.setter
def last_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._last_name = value
重復(fù)代碼會導(dǎo)致臃腫、易出錯和丑陋的程序。好消息是,通過使用裝飾器或閉包,有很多種更好的方法來完成同樣的事情。 可以參考8.9和9.21小節(jié)的內(nèi)容。
你想在子類中調(diào)用父類的某個已經(jīng)被覆蓋的方法。
為了調(diào)用父類(超類)的一個方法,可以使用 super() 函數(shù),比如:
class A:
def spam(self):
print('A.spam')
class B(A):
def spam(self):
print('B.spam')
super().spam() # Call parent spam()
super()函數(shù)的一個常見用法是在 __init__()方法中確保父類被正確的初始化了:
class A:
def __init__(self):
self.x = 0
class B(A):
def __init__(self):
super().__init__()
self.y = 1
super() 的另外一個常見用法出現(xiàn)在覆蓋 Python 特殊方法的代碼中,比如:
class Proxy:
def __init__(self, obj):
self._obj = obj
# Delegate attribute lookup to internal obj
def __getattr__(self, name):
return getattr(self._obj, name)
# Delegate attribute assignment
def __setattr__(self, name, value):
if name.startswith('_'):
super().__setattr__(name, value) # Call original __setattr__
else:
setattr(self._obj, name, value)
在上面代碼中,__setattr__()的實現(xiàn)包含一個名字檢查。 如果某個屬性名以下劃線(_)開頭,就通過super() 調(diào)用原始的 __setattr__() , 否則的話就委派給內(nèi)部的代理對象self._obj去處理。 這看上去有點意思,因為就算沒有顯式的指明某個類的父類, super() 仍然可以有效的工作。
實際上,大家對于在Python中如何正確使用 super()函數(shù)普遍知之甚少。 你有時候會看到像下面這樣直接調(diào)用父類的一個方法:
class Base:
def __init__(self):
print('Base.__init__')
class A(Base):
def __init__(self):
Base.__init__(self)
print('A.__init__')
盡管對于大部分代碼而言這么做沒什么問題,但是在更復(fù)雜的涉及到多繼承的代碼中就有可能導(dǎo)致很奇怪的問題發(fā)生。 比如,考慮如下的情況:
class Base:
def __init__(self):
print('Base.__init__')
class A(Base):
def __init__(self):
Base.__init__(self)
print('A.__init__')
class B(Base):
def __init__(self):
Base.__init__(self)
print('B.__init__')
class C(A,B):
def __init__(self):
A.__init__(self)
B.__init__(self)
print('C.__init__')
如果你運行這段代碼就會發(fā)現(xiàn) Base.__init__() 被調(diào)用兩次,如下所示:
>>> c = C()
Base.__init__
A.__init__
Base.__init__
B.__init__
C.__init__
>>>
可能兩次調(diào)用Base.__init__()沒什么壞處,但有時候卻不是。 另一方面,假設(shè)你在代碼中換成使用 super() ,結(jié)果就很完美了:
class Base:
def __init__(self):
print('Base.__init__')
class A(Base):
def __init__(self):
super().__init__()
print('A.__init__')
class B(Base):
def __init__(self):
super().__init__()
print('B.__init__')
class C(A,B):
def __init__(self):
super().__init__() # Only one call to super() here
print('C.__init__')
運行這個新版本后,你會發(fā)現(xiàn)每個__init__()方法只會被調(diào)用一次了:
>>> c = C()
Base.__init__
B.__init__
A.__init__
C.__init__
>>>
為了弄清它的原理,我們需要花點時間解釋下 Python 是如何實現(xiàn)繼承的。 對于你定義的每一個類而已,Python 會計算出一個所謂的方法解析順序(MRO)列表。 這個 MRO 列表就是一個簡單的所有基類的線性順序表。例如:
>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
<class '__main__.Base'>, <class 'object'>)
>>>
為了實現(xiàn)繼承,Python 會在 MRO 列表上從左到右開始查找基類,直到找到第一個匹配這個屬性的類為止。
而這個 MRO 列表的構(gòu)造是通過一個 C3 線性化算法來實現(xiàn)的。 我們不去深究這個算法的數(shù)學(xué)原理,它實際上就是合并所有父類的 MRO 列表并遵循如下三條準(zhǔn)則:
當(dāng)你使用 super() 函數(shù)時,Python 會在 MRO 列表上繼續(xù)搜索下一個類。 只要每個重定義的方法統(tǒng)一使用 super()并只調(diào)用它一次, 那么控制流最終會遍歷完整個 MRO 列表,每個方法也只會被調(diào)用一次。 這也是為什么在第二個例子中你不會調(diào)用兩次 Base.__init__() 的原因。
super() 有個令人吃驚的地方是它并不一定去查找某個類在 MRO 中下一個直接父類, 你甚至可以在一個沒有直接父類的類中使用它。例如,考慮如下這個類:
class A:
def spam(self):
print('A.spam')
super().spam()
如果你試著直接使用這個類就會出錯:
>>> a = A()
>>> a.spam()
A.spam
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in spam
AttributeError: 'super' object has no attribute 'spam'
>>>
但是,如果你使用多繼承的話看看會發(fā)生什么:
>>> class B:
... def spam(self):
... print('B.spam')
...
>>> class C(A,B):
... pass
...
>>> c = C()
>>> c.spam()
A.spam
B.spam
>>>
你可以看到在類 A 中使用 super().spam() 實際上調(diào)用的是跟類 A 毫無關(guān)系的類 B 中的 spam() 方法。 這個用類 C 的 MRO 列表就可以完全解釋清楚了:
>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
<class 'object'>)
>>>
在定義混入類的時候這樣使用super() 是很普遍的。可以參考8.13和8.18小節(jié)。
然而,由于super() 可能會調(diào)用不是你想要的方法,你應(yīng)該遵循一些通用原則。 首先,確保在繼承體系中所有相同名字的方法擁有可兼容的參數(shù)簽名(比如相同的參數(shù)個數(shù)和參數(shù)名稱)。 這樣可以確保 super() 調(diào)用一個非直接父類方法時不會出錯。 其次,最好確保最頂層的類提供了這個方法的實現(xiàn),這樣的話在 MRO 上面的查找鏈肯定可以找到某個確定的方法。
在 Python 社區(qū)中對于super() 的使用有時候會引來一些爭議。 盡管如此,如果一切順利的話,你應(yīng)該在你最新代碼中使用它。 Raymond Hettinger 為此寫了一篇非常好的文章 “Python’s super() Considered Super!” , 通過大量的例子向我們解釋了為什么super() 是極好的。
在子類中,你想要擴展定義在父類中的 property 的功能。
考慮如下的代碼,它定義了一個 property:
class Person:
def __init__(self, name):
self.name = name
# Getter function
@property
def name(self):
return self._name
# Setter function
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._name = value
# Deleter function
@name.deleter
def name(self):
raise AttributeError("Can't delete attribute")
下面是一個示例類,它繼承自 Person 并擴展了 name屬性的功能:
class SubPerson(Person):
@property
def name(self):
print('Getting name')
return super().name
@name.setter
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
@name.deleter
def name(self):
print('Deleting name')
super(SubPerson, SubPerson).name.__delete__(self)
接下來使用這個新類:
>>> s = SubPerson('Guido')
Setting name to Guido
>>> s.name
Getting name
'Guido'
>>> s.name = 'Larry'
Setting name to Larry
>>> s.name = 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 16, in name
raise TypeError('Expected a string')
TypeError: Expected a string
>>>
如果你僅僅只想擴展 property 的某一個方法,那么可以像下面這樣寫:
class SubPerson(Person):
@Person.name.getter
def name(self):
print('Getting name')
return super().name
或者,你只想修改 setter 方法,就這么寫:
class SubPerson(Person):
@Person.name.setter
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
在子類中擴展一個 property 可能會引起很多不易察覺的問題, 因為一個 property 其實是getter、setter 和 deleter方法的集合,而不是單個方法。 因此,但你擴展一個property的時候,你需要先確定你是否要重新定義所有的方法還是說只修改其中某一個。
在第一個例子中,所有的 property 方法都被重新定義。 在每一個方法中,使用了 super()來調(diào)用父類的實現(xiàn)。 在 setter 函數(shù)中使用super(SubPerson, SubPerson).name.__set__(self, value)的語句是沒有錯的。 為了委托給之前定義的 setter 方法,需要將控制權(quán)傳遞給之前定義的 name 屬性的__set__()方法。 不過,獲取這個方法的唯一途徑是使用類變量而不是實例變量來訪問它。 這也是為什么我們要使用 super(SubPerson, SubPerson) 的原因。
如果你只想重定義其中一個方法,那只使用 @property 本身是不夠的。比如,下面的代碼就無法工作:
class SubPerson(Person):
@property # Doesn't work
def name(self):
print('Getting name')
return super().name
如果你試著運行會發(fā)現(xiàn) setter 函數(shù)整個消失了:
>>> s = SubPerson('Guido')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 5, in __init__
self.name = name
AttributeError: can't set attribute
>>>
你應(yīng)該像之前說過的那樣修改代碼:
class SubPerson(Person):
@Person.getter
def name(self):
print('Getting name')
return super().name
這么寫后,property 之前已經(jīng)定義過的方法會被復(fù)制過來,而 getter 函數(shù)被替換。然后它就能按照期望的工作了:
>>> s = SubPerson('Guido')
>>> s.name
Getting name
'Guido'
>>> s.name = 'Larry'
>>> s.name
Getting name
'Larry'
>>> s.name = 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 16, in name
raise TypeError('Expected a string')
TypeError: Expected a string
>>>
在這個特別的解決方案中,我們沒辦法使用更加通用的方式去替換硬編碼的Person類名。 如果你不知道到底是哪個基類定義了 property, 那你只能通過重新定義所有 property 并使用 super()來將控制權(quán)傳遞給前面的實現(xiàn)。
值的注意的是上面演示的第一種技術(shù)還可以被用來擴展一個描述器(在8.9小節(jié)我們有專門的介紹)。比如:
# A descriptor
class String:
def __init__(self, name):
self.name = name
def __get__(self, instance, cls):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
instance.__dict__[self.name] = value
# A class with a descriptor
class Person:
name = String('name')
def __init__(self, name):
self.name = name
# Extending a descriptor with a property
class SubPerson(Person):
@property
def name(self):
print('Getting name')
return super().name
@name.setter
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
@name.deleter
def name(self):
print('Deleting name')
super(SubPerson, SubPerson).name.__delete__(self)
最后值的注意的是,讀到這里時,你應(yīng)該會發(fā)現(xiàn)子類化 setter 和 deleter方法其實是很簡單的。 這里演示的解決方案同樣適用,但是在 [http://bugs.python.org/issue14965](Python 的 issue 頁面)報告的一個 bug,或許會使得將來的 Python 版本中出現(xiàn)一個更加簡潔的方法。
你想創(chuàng)建一個新的擁有一些額外功能的實例屬性類型,比如類型檢查。
如果你想創(chuàng)建一個全新的實例屬性,可以通過一個描述器類的形式來定義它的功能。下面是一個例子:
# Descriptor attribute for an integer type-checked attribute
class Integer:
def __init__(self, name):
self.name = name
def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Expected an int')
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
一個描述器就是一個實現(xiàn)了三個核心的屬性訪問操作(get, set, delete)的類, 分別為__get__() 、__set__() 和 __delete__() 這三個特殊的方法。 這些方法接受一個實例作為輸入,之后相應(yīng)的操作實例底層的字典。
為了使用一個描述器,需將這個描述器的實例作為類屬性放到一個類的定義中。例如:
class Point:
x = Integer('x')
y = Integer('y')
def __init__(self, x, y):
self.x = x
self.y = y
當(dāng)你這樣做后,所有隊描述器屬性(比如 x 或 y)的訪問會被 __get__()、__set__() 和__delete__()方法捕獲到。例如:
>>> p = Point(2, 3)
>>> p.x # Calls Point.x.__get__(p,Point)
2
>>> p.y = 5 # Calls Point.y.__set__(p, 5)
>>> p.x = 2.3 # Calls Point.x.__set__(p, 2.3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "descrip.py", line 12, in __set__
raise TypeError('Expected an int')
TypeError: Expected an int
>>>
作為輸入,描述器的每一個方法會接受一個操作實例。 為了實現(xiàn)請求操作,會相應(yīng)的操作實例底層的字典(dict屬性)。 描述器的 self.name 屬性存儲了在實例字典中被實際使用到的 key。
描述器可實現(xiàn)大部分Python類特性中的底層魔法, 包括@classmethod 、@staticmethod 、@property ,甚至是__slots__特性。
通過定義一個描述器,你可以在底層捕獲核心的實例操作(get, set, delete),并且可完全自定義它們的行為。 這是一個強大的工具,有了它你可以實現(xiàn)很多高級功能,并且它也是很多高級庫和框架中的重要工具之一。
描述器的一個比較困惑的地方是它只能在類級別被定義,而不能為每個實例單獨定義。因此,下面的代碼是無法工作的:
# Does NOT work
class Point:
def __init__(self, x, y):
self.x = Integer('x') # No! Must be a class variable
self.y = Integer('y')
self.x = x
self.y = y
同時,__get__()方法實現(xiàn)起來比看上去要復(fù)雜得多:
# Descriptor attribute for an integer type-checked attribute
class Integer:
def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]
__get__() 看上去有點復(fù)雜的原因歸結(jié)于實例變量和類變量的不同。 如果一個描述器被當(dāng)做一個類變量來訪問,那么 instance 參數(shù)被設(shè)置成 None 。 這種情況下,標(biāo)準(zhǔn)做法就是簡單的返回這個描述器本身即可(盡管你還可以添加其他的自定義操作)。例如:
>>> p = Point(2,3)
>>> p.x # Calls Point.x.__get__(p, Point)
2
>>> Point.x # Calls Point.x.__get__(None, Point)
<__main__.Integer object at 0x100671890>
>>>
描述器通常是那些使用到裝飾器或元類的大型框架中的一個組件。同時它們的使用也被隱藏在后面。 舉個例子,下面是一些更高級的基于描述器的代碼,并涉及到一個類裝飾器:
# Descriptor for a type-checked attribute
class Typed:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError('Expected ' + str(self.expected_type))
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
# Class decorator that applies it to selected attributes
def typeassert(**kwargs):
def decorate(cls):
for name, expected_type in kwargs.items():
# Attach a Typed descriptor to the class
setattr(cls, name, Typed(name, expected_type))
return cls
return decorate
# Example use
@typeassert(name=str, shares=int, price=float)
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
最后要指出的一點是,如果你只是想簡單的自定義某個類的單個屬性訪問的話就不用去寫描述器了。 這種情況下使用8.6小節(jié)介紹的 property 技術(shù)會更加容易。 當(dāng)程序中有很多重復(fù)代碼的時候描述器就很有用了 (比如你想在你代碼的很多地方使用描述器提供的功能或者將它作為一個函數(shù)庫特性)。
你想將一個只讀屬性定義成一個 property,并且只在訪問的時候才會計算結(jié)果。 但是一旦被訪問后,你希望結(jié)果值被緩存起來,不用每次都去計算。
定義一個延遲屬性的一種高效方法是通過使用一個描述器類,如下所示:
class lazyproperty:
def __init__(self, func):
self.func = func
def __get__(self, instance, cls):
if instance is None:
return self
else:
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
你需要像下面這樣在一個類中使用它:
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@lazyproperty
def area(self):
print('Computing area')
return math.pi * self.radius ** 2
@lazyproperty
def perimeter(self):
print('Computing perimeter')
return 2 * math.pi * self.radius
下面在一個交互環(huán)境中演示它的使用:
>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area
Computing area
50.26548245743669
>>> c.area
50.26548245743669
>>> c.perimeter
Computing perimeter
25.132741228718345
>>> c.perimeter
25.132741228718345
>>>
仔細觀察你會發(fā)現(xiàn)消息 Computing area 和 Computing perimeter 僅僅出現(xiàn)一次。
很多時候,構(gòu)造一個延遲計算屬性的主要目的是為了提升性能。 例如,你可以避免計算這些屬性值,除非你真的需要它們。 這里演示的方案就是用來實現(xiàn)這樣的效果的, 只不過它是通過以非常高效的方式使用描述器的一個精妙特性來達到這種效果的。
正如在其他小節(jié)(如8.9小節(jié))所講的那樣,當(dāng)一個描述器被放入一個類的定義時, 每次訪問屬性時它的 __get__()、__set__() 和__delete__()方法就會被觸發(fā)。 不過,如果一個描述器僅僅只定義了一個__get__() 方法的話,它比通常的具有更弱的綁定。 特別地,只有當(dāng)被訪問屬性不在實例底層的字典中時__get__()方法才會被觸發(fā)。
lazyproperty類利用這一點,使用 __get__() 方法在實例中存儲計算出來的值, 這個實例使用相同的名字作為它的 property。 這樣一來,結(jié)果值被存儲在實例字典中并且以后就不需要再去計算這個 property 了。 你可以嘗試更深入的例子來觀察結(jié)果:
>>> c = Circle(4.0)
>