鍍金池/ 教程/ Python/ 類與對(duì)象
類與對(duì)象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時(shí)間
測(cè)試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語(yǔ)言擴(kuò)展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

類與對(duì)象

本章主要關(guān)注點(diǎn)的是和類定義有關(guān)的常見編程模型。包括讓對(duì)象支持常見的 Python 特性、特殊方法的使用、 類封裝技術(shù)、繼承、內(nèi)存管理以及有用的設(shè)計(jì)模式。

改變對(duì)象的字符串顯示

問(wèn)題

你想改變對(duì)象實(shí)例的打印或顯示輸出,讓它們更具可讀性。

解決方案

要改變一個(gè)實(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è)實(shí)例的代碼表示形式,通常用來(lái)重新構(gòu)造這個(gè)實(shí)例。 內(nèi)置的 repr()函數(shù)返回這個(gè)字符串,跟我們使用交互式解釋器顯示的值是一樣的。 __str__()方法將實(shí)例轉(zhuǎn)換為一個(gè)字符串,使用 str()print() 函數(shù)會(huì)輸出這個(gè)字符串。比如:

>>> p = Pair(3, 4)
>>> p
Pair(3, 4) # __repr__() output
>>> print(p)
(3, 4) # __str__() output
>>>

我們?cè)谶@里還演示了在格式化的時(shí)候怎樣使用不同的字符串表現(xiàn)形式。 特別來(lái)講,!r 格式化代碼指明輸出使用__repr__() 來(lái)代替默認(rèn)的__str__() 。 你可以用前面的類來(lái)試著測(cè)試下:

>>> 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í)慣,因?yàn)樗芎?jiǎn)化調(diào)試和實(shí)例輸出。 例如,如果僅僅只是打印輸出或日志輸出某個(gè)實(shí)例,那么程序員會(huì)看到實(shí)例更加詳細(xì)與有用的信息。

__repr__() 生成的文本字符串標(biāo)準(zhǔn)做法是需要讓 eval(repr(x)) == x為真。 如果實(shí)在不能這樣子做,應(yīng)該創(chuàng)建一個(gè)有用的文本表示,并使用 < 和 > 括起來(lái)。比如:

>>> f = open('file.dat')
>>> f
<_io.TextIOWrapper name='file.dat' mode='r' encoding='UTF-8'>
>>>

如果 __str__() 沒(méi)有被定義,那么就會(huì)使用 __repr__() 來(lái)代替輸出。

上面的 format()方法的使用看上去很有趣,格式化代碼 {0.x}對(duì)應(yīng)的是第1個(gè)參數(shù)的 x 屬性。 因此,在下面的函數(shù)中,0實(shí)際上指的就是 self本身:

def __repr__(self):
    return 'Pair({0.x!r}, {0.y!r})'.format(self)

作為這種實(shí)現(xiàn)的一個(gè)替代,你也可以使用 % 操作符,就像下面這樣:

def __repr__(self):
    return 'Pair(%r, %r)' % (self.x, self.y)

自定義字符串的格式化

問(wèn)題

你想通過(guò) format()函數(shù)和字符串方法使得一個(gè)對(duì)象能支持自定義的格式化。

解決方案

為了自定義字符串的格式化,我們需要在類上面定義__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類的實(shí)例可以支持格式化操作了,如同下面這樣:

>>> 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 的字符串格式化功能提供了一個(gè)鉤子。 這里需要著重強(qiáng)調(diào)的是格式化代碼的解析工作完全由類自己決定。因此,格式化代碼可以是任何值。 例如,參考下面來(lái)自 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'
>>>

對(duì)于內(nèi)置類型的格式化有一些標(biāo)準(zhǔn)的約定。 可以參考 string 模塊文檔說(shuō)明。

讓對(duì)象支持上下文管理協(xié)議

問(wèn)題

你想讓你的對(duì)象支持上下文管理協(xié)議(with 語(yǔ)句)。

解決方案

為了讓一個(gè)對(duì)象兼容 with語(yǔ)句,你需要實(shí)現(xiàn) __enter__()__exit__() 方法。 例如,考慮如下的一個(gè)類,它能為我們創(chuàng)建一個(gè)網(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

這個(gè)類的關(guān)鍵特點(diǎn)在于它表示了一個(gè)網(wǎng)絡(luò)連接,但是初始化的時(shí)候并不會(huì)做任何事情(比如它并沒(méi)有建立一個(gè)連接)。 連接的建立和關(guān)閉是使用 with語(yǔ)句自動(dòng)完成的,例如:

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

討論

編寫上下文管理器的主要原理是你的代碼會(huì)放到with語(yǔ)句塊中執(zhí)行。 當(dāng)出現(xiàn) with 語(yǔ)句的時(shí)候,對(duì)象的 __enter__()方法被觸發(fā), 它返回的值(如果有的話)會(huì)被賦值給 as 聲明的變量。然后,with 語(yǔ)句塊里面的代碼開始執(zhí)行。 最后,__exit__() 方法被觸發(fā)進(jìn)行清理工作。

不管 with代碼塊中發(fā)生什么,上面的控制流都會(huì)執(zhí)行完,就算代碼塊中發(fā)生了異常也是一樣的。 事實(shí)上,__exit__() 方法的第三個(gè)參數(shù)包含了異常類型、異常值和追溯信息(如果有的話)。 __exit__() 方法能自己決定怎樣利用這個(gè)異常信息,或者忽略它并返回一個(gè) None 值。 如果 __exit__() 返回 True ,那么異常會(huì)被清空,就好像什么都沒(méi)發(fā)生一樣, with語(yǔ)句后面的程序繼續(xù)在正常執(zhí)行。

還有一個(gè)細(xì)節(jié)問(wèn)題就是 LazyConnection 類是否允許多個(gè) with 語(yǔ)句來(lái)嵌套使用連接。 很顯然,上面的定義中一次只能允許一個(gè) socket 連接,如果正在使用一個(gè) socket 的時(shí)候又重復(fù)使用 with 語(yǔ)句, 就會(huì)產(chǎn)生一個(gè)異常了。不過(guò)你可以像下面這樣修改下上面的實(shí)現(xiàn)來(lái)解決這個(gè)問(wè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

在第二個(gè)版本中,LazyConnection 類可以被看做是某個(gè)連接工廠。在內(nèi)部,一個(gè)列表被用來(lái)構(gòu)造一個(gè)棧。 每次 __enter__() 方法執(zhí)行的時(shí)候,它復(fù)制創(chuàng)建一個(gè)新的連接并將其加入到棧里面。 __exit__() 方法簡(jiǎn)單的從棧中彈出最后一個(gè)連接并關(guān)閉它。 這里稍微有點(diǎn)難理解,不過(guò)它能允許嵌套使用 with 語(yǔ)句創(chuàng)建多個(gè)連接,就如上面演示的那樣。

在需要管理一些資源比如文件、網(wǎng)絡(luò)連接和鎖的編程環(huán)境中,使用上下文管理器是很普遍的。 這些資源的一個(gè)主要特征是它們必須被手動(dòng)的關(guān)閉或釋放來(lái)確保程序的正確運(yùn)行。 例如,如果你請(qǐng)求了一個(gè)鎖,那么你必須確保之后釋放了它,否則就可能產(chǎn)生死鎖。 通過(guò)實(shí)現(xiàn)__enter__()__exit__() 方法并使用with 語(yǔ)句可以很容易的避免這些問(wèn)題, 因?yàn)?__exit__()方法可以讓你無(wú)需擔(dān)心這些了。

contextmanager模塊中有一個(gè)標(biāo)準(zhǔn)的上下文管理方案模板,可參考9.22小節(jié)。 同時(shí)在12.6小節(jié)中還有一個(gè)對(duì)本節(jié)示例程序的線程安全的修改版。

創(chuàng)建大量對(duì)象時(shí)節(jié)省內(nèi)存方法

問(wèn)題

你的程序要?jiǎng)?chuàng)建大量(可能上百萬(wàn))的對(duì)象,導(dǎo)致占用很大的內(nèi)存。

解決方案

對(duì)于主要是用來(lái)當(dāng)成簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)的類而言,你可以通過(guò)給類添加 __slots__ 屬性來(lái)極大的減少實(shí)例所占的內(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 就會(huì)為實(shí)例使用一種更加緊湊的內(nèi)部表示。 實(shí)例通過(guò)一個(gè)很小的固定大小的數(shù)組來(lái)構(gòu)建,而不是為每個(gè)實(shí)例定義一個(gè)字典,這跟元組或列表很類似。 在__slots__ 中列出的屬性名在內(nèi)部被映射到這個(gè)數(shù)組的指定小標(biāo)上。 使用 slots 一個(gè)不好的地方就是我們不能再給實(shí)例添加新的屬性了,只能使用在 __slots__中定義的那些屬性名。

討論

使用 slots 后節(jié)省的內(nèi)存會(huì)跟存儲(chǔ)屬性的數(shù)量和類型有關(guān)。 不過(guò),一般來(lái)講,使用到的內(nèi)存總量和將數(shù)據(jù)存儲(chǔ)在一個(gè)元組中差不多。 為了給你一個(gè)直觀認(rèn)識(shí),假設(shè)你不使用 slots 直接存儲(chǔ)一個(gè) Date 實(shí)例, 在64位的 Python 上面要占用428字節(jié),而如果使用了 slots,內(nèi)存占用下降到156字節(jié)。 如果程序中需要同時(shí)創(chuàng)建大量的日期實(shí)例,那么這個(gè)就能極大的減小內(nèi)存使用量了。

盡管 slots 看上去是一個(gè)很有用的特性,很多時(shí)候你還是得減少對(duì)它的使用沖動(dòng)。 Python 的很多特性都依賴于普通的基于字典的實(shí)現(xiàn)。 另外,定義了 slots 后的類不再支持一些普通類特性了,比如多繼承。 大多數(shù)情況下,你應(yīng)該只在那些經(jīng)常被使用到的用作數(shù)據(jù)結(jié)構(gòu)的類上定義 slots (比如在程序中需要?jiǎng)?chuàng)建某個(gè)類的幾百萬(wàn)個(gè)實(shí)例對(duì)象)。

關(guān)于 __slots__ 的一個(gè)常見誤區(qū)是它可以作為一個(gè)封裝工具來(lái)防止用戶給實(shí)例增加新的屬性。 盡管使用 slots 可以達(dá)到這樣的目的,但是這個(gè)并不是它的初衷。 __slots__ 更多的是用來(lái)作為一個(gè)內(nèi)存優(yōu)化工具。

在類中封裝屬性名

問(wèn)題

你想封裝類的實(shí)例上面的“私有”數(shù)據(jù),但是 Python 語(yǔ)言并沒(méi)有訪問(wèn)控制。

解決方案

Python 程序員不去依賴語(yǔ)言特性去封裝數(shù)據(jù),而是通過(guò)遵循一定的屬性和方法命名規(guī)約來(lái)達(dá)到這個(gè)效果。 第一個(gè)約定是任何以單下劃線_開頭的名字都應(yīng)該是內(nèi)部實(shí)現(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 并不會(huì)真的阻止別人訪問(wèn)內(nèi)部名稱。但是如果你這么做肯定是不好的,可能會(huì)導(dǎo)致脆弱的代碼。 同時(shí)還要注意到,使用下劃線開頭的約定同樣適用于模塊名和模塊級(jí)別函數(shù)。 例如,如果你看到某個(gè)模塊名以單下劃線開頭(比如_socket),那它就是內(nèi)部實(shí)現(xiàn)。 類似的,模塊級(jí)別函數(shù)比如 sys._getframe()在使用的時(shí)候就得加倍小心了。

你還可能會(huì)遇到在類定義中使用兩個(gè)下劃線(__)開頭的命名。比如:

class B:
    def __init__(self):
        self.__private = 0

    def __private_method(self):
        pass

    def public_method(self):
        pass
        self.__private_method()

使用雙下劃線開始會(huì)導(dǎo)致訪問(wèn)名稱變成其他形式。 比如,在前面的類B中,私有屬性會(huì)被分別重命名為_B__private_B__private_method。 這時(shí)候你可能會(huì)問(wèn)這樣重命名的目的是什么,答案就是繼承——這種屬性通過(guò)繼承是無(wú)法被覆蓋的。比如:

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 ,這個(gè)跟父類 B 中的名稱是完全不同的。

討論

上面提到有兩種不同的編碼約定(單下劃線和雙下劃線)來(lái)命名私有屬性,那么問(wèn)題就來(lái)了:到底哪種方式好呢? 大多數(shù)而言,你應(yīng)該讓你的非公共名稱以單下劃線開頭。但是,如果你清楚你的代碼會(huì)涉及到子類, 并且有些內(nèi)部屬性應(yīng)該在子類中隱藏起來(lái),那么才考慮使用雙下劃線方案。

還有一點(diǎn)要注意的是,有時(shí)候你定義的一個(gè)變量和某個(gè)保留關(guān)鍵字沖突,這時(shí)候可以使用單下劃線作為后綴,例如:

lambda_ = 2.0 # Trailing _ to avoid clash with lambda keyword

這里我們并不使用單下劃線前綴的原因是它避免誤解它的使用初衷 (如使用單下劃線前綴的目的是為了防止命名沖突而不是指明這個(gè)屬性是私有的)。 通過(guò)使用單下劃線后綴可以解決這個(gè)問(wèn)題。

創(chuàng)建可管理的屬性

問(wèn)題

你想給某個(gè)實(shí)例 attribute 增加除訪問(wèn)與修改之外的其他處理邏輯,比如類型檢查或合法性驗(yàn)證。

解決方案

自定義某個(gè)屬性的一種簡(jiǎn)單方法是將它定義為一個(gè) property。 例如,下面的代碼定義了一個(gè) property,增加對(duì)一個(gè)屬性簡(jiǎn)單的類型檢查:

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")

上述代碼中有三個(gè)相關(guān)聯(lián)的方法,這三個(gè)方法的名字都必須一樣。 第一個(gè)方法是一個(gè) getter 函數(shù),它使得 first_name 成為一個(gè)屬性。 其他兩個(gè)方法給 first_name 屬性添加了 setterdeleter函數(shù)。 需要強(qiáng)調(diào)的是只有在 first_name屬性被創(chuàng)建后, 后面的兩個(gè)裝飾器@first_name.setter@first_name.deleter才能被定義。

property 的一個(gè)關(guān)鍵特征是它看上去跟普通的 attribute 沒(méi)什么兩樣, 但是訪問(wèn)它的時(shí)候會(huì)自動(dòng)觸發(fā) getter、setterdeleter 方法。例如:

>>> 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
>>>

在實(shí)現(xiàn)一個(gè) property 的時(shí)候,底層數(shù)據(jù)(如果有的話)仍然需要存儲(chǔ)在某個(gè)地方。 因此,在 get 和 set 方法中,你會(huì)看到對(duì) _first_name 屬性的操作,這也是實(shí)際數(shù)據(jù)保存的地方。 另外,你可能還會(huì)問(wèn)為什么 __init__()方法中設(shè)置了 self.first_name而不是 self._first_name。 在這個(gè)例子中,我們創(chuàng)建一個(gè) property 的目的就是在設(shè)置 attribute 的時(shí)候進(jìn)行檢查。 因此,你可能想在初始化的時(shí)候也進(jìn)行這種類型檢查。通過(guò)設(shè)置 self.first_name ,自動(dòng)調(diào)用 setter方法, 這個(gè)方法里面會(huì)進(jìn)行參數(shù)的檢查,否則就是直接訪問(wèn)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)

討論

一個(gè) property 屬性其實(shí)就是一系列相關(guān)綁定方法的集合。如果你去查看擁有 property 的類, 就會(huì)發(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>
>>>

通常來(lái)講,你不會(huì)直接取調(diào)用 fget 或者 fset,它們會(huì)在訪問(wèn) property 的時(shí)候自動(dòng)被觸發(fā)。

只有當(dāng)你確實(shí)需要對(duì) attribute 執(zhí)行其他額外的操作的時(shí)候才應(yīng)該使用到 property。 有時(shí)候一些從其他編程語(yǔ)言(比如 Java)過(guò)來(lái)的程序員總認(rèn)為所有訪問(wèn)都應(yīng)該通過(guò) getter 和 setter, 所以他們認(rèn)為代碼應(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

不要寫這種沒(méi)有做任何其他額外操作的 property。 首先,它會(huì)讓你的代碼變得很臃腫,并且還會(huì)迷惑閱讀者。 其次,它還會(huì)讓你的程序運(yùn)行起來(lái)變慢很多。 最后,這樣的設(shè)計(jì)并沒(méi)有帶來(lái)任何的好處。 特別是當(dāng)你以后想給普通 attribute 訪問(wèn)添加額外的處理邏輯的時(shí)候, 你可以將它變成一個(gè) property 而無(wú)需改變?cè)瓉?lái)的代碼。 因?yàn)樵L問(wèn) attribute 的代碼還是保持原樣。

Properties 還是一種定義動(dòng)態(tài)計(jì)算 attribute 的方法。 這種類型的 attributes 并不會(huì)被實(shí)際的存儲(chǔ),而是在需要的時(shí)候計(jì)算出來(lái)。比如:

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

在這里,我們通過(guò)使用 properties,將所有的訪問(wèn)接口形式統(tǒng)一起來(lái), 對(duì)半徑、直徑、周長(zhǎng)和面積的訪問(wèn)都是通過(guò)屬性訪問(wèn),就跟訪問(wèn)簡(jiǎn)單的 attribute 是一樣的。 如果不這樣做的話,那么就要在代碼中混合使用簡(jiǎn)單屬性訪問(wèn)和方法調(diào)用。 下面是使用的實(shí)例:

>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area  # Notice lack of ()
50.26548245743669
>>> c.perimeter  # Notice lack of ()
25.132741228718345
>>>

盡管 properties 可以實(shí)現(xiàn)優(yōu)雅的編程接口,但有些時(shí)候你還是會(huì)想直接使用 getter 和 setter 函數(shù)。例如:

>>> p = Person('Guido')
>>> p.get_first_name()
'Guido'
>>> p.set_first_name('Larry')
>>>

這種情況的出現(xiàn)通常是因?yàn)?Python 代碼被集成到一個(gè)大型基礎(chǔ)平臺(tái)架構(gòu)或程序中。 例如,有可能是一個(gè) Python 類準(zhǔn)備加入到一個(gè)基于遠(yuǎn)程過(guò)程調(diào)用的大型分布式系統(tǒng)中。 這種情況下,直接使用 get/set 方法(普通方法調(diào)用)而不是 property 或許會(huì)更容易兼容。

最后一點(diǎn),不要像下面這樣寫有大量重復(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ù)代碼會(huì)導(dǎo)致臃腫、易出錯(cuò)和丑陋的程序。好消息是,通過(guò)使用裝飾器或閉包,有很多種更好的方法來(lái)完成同樣的事情。 可以參考8.9和9.21小節(jié)的內(nèi)容。

調(diào)用父類方法

問(wèn)題

你想在子類中調(diào)用父類的某個(gè)已經(jīng)被覆蓋的方法。

解決方案

為了調(diào)用父類(超類)的一個(gè)方法,可以使用 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ù)的一個(gè)常見用法是在 __init__()方法中確保父類被正確的初始化了:

class A:
    def __init__(self):
        self.x = 0

class B(A):
    def __init__(self):
        super().__init__()
        self.y = 1

super() 的另外一個(gè)常見用法出現(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__()的實(shí)現(xiàn)包含一個(gè)名字檢查。 如果某個(gè)屬性名以下劃線(_)開頭,就通過(guò)super() 調(diào)用原始的 __setattr__() , 否則的話就委派給內(nèi)部的代理對(duì)象self._obj去處理。 這看上去有點(diǎn)意思,因?yàn)榫退銢](méi)有顯式的指明某個(gè)類的父類, super() 仍然可以有效的工作。

討論

實(shí)際上,大家對(duì)于在Python中如何正確使用 super()函數(shù)普遍知之甚少。 你有時(shí)候會(huì)看到像下面這樣直接調(diào)用父類的一個(gè)方法:

class Base:
    def __init__(self):
        print('Base.__init__')

class A(Base):
    def __init__(self):
        Base.__init__(self)
        print('A.__init__')

盡管對(duì)于大部分代碼而言這么做沒(méi)什么問(wèn)題,但是在更復(fù)雜的涉及到多繼承的代碼中就有可能導(dǎo)致很奇怪的問(wèn)題發(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__')

如果你運(yùn)行這段代碼就會(huì)發(fā)現(xiàn) Base.__init__() 被調(diào)用兩次,如下所示:

>>> c = C()
Base.__init__
A.__init__
Base.__init__
B.__init__
C.__init__
>>>

可能兩次調(diào)用Base.__init__()沒(méi)什么壞處,但有時(shí)候卻不是。 另一方面,假設(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__')

運(yùn)行這個(gè)新版本后,你會(huì)發(fā)現(xiàn)每個(gè)__init__()方法只會(huì)被調(diào)用一次了:

>>> c = C()
Base.__init__
B.__init__
A.__init__
C.__init__
>>>

為了弄清它的原理,我們需要花點(diǎn)時(shí)間解釋下 Python 是如何實(shí)現(xiàn)繼承的。 對(duì)于你定義的每一個(gè)類而已,Python 會(huì)計(jì)算出一個(gè)所謂的方法解析順序(MRO)列表。 這個(gè) MRO 列表就是一個(gè)簡(jiǎn)單的所有基類的線性順序表。例如:

>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
<class '__main__.Base'>, <class 'object'>)
>>>

為了實(shí)現(xiàn)繼承,Python 會(huì)在 MRO 列表上從左到右開始查找基類,直到找到第一個(gè)匹配這個(gè)屬性的類為止。

而這個(gè) MRO 列表的構(gòu)造是通過(guò)一個(gè) C3 線性化算法來(lái)實(shí)現(xiàn)的。 我們不去深究這個(gè)算法的數(shù)學(xué)原理,它實(shí)際上就是合并所有父類的 MRO 列表并遵循如下三條準(zhǔn)則:

  • 子類會(huì)先于父類被檢查
  • 多個(gè)父類會(huì)根據(jù)它們?cè)诹斜碇械捻樞虮粰z查
  • 如果對(duì)下一個(gè)類存在兩個(gè)合法的選擇,選擇第一個(gè)父類 老實(shí)說(shuō),你所要知道的就是 MRO 列表中的類順序會(huì)讓你定義的任意類層級(jí)關(guān)系變得有意義。

當(dāng)你使用 super() 函數(shù)時(shí),Python 會(huì)在 MRO 列表上繼續(xù)搜索下一個(gè)類。 只要每個(gè)重定義的方法統(tǒng)一使用 super()并只調(diào)用它一次, 那么控制流最終會(huì)遍歷完整個(gè) MRO 列表,每個(gè)方法也只會(huì)被調(diào)用一次。 這也是為什么在第二個(gè)例子中你不會(huì)調(diào)用兩次 Base.__init__() 的原因。

super() 有個(gè)令人吃驚的地方是它并不一定去查找某個(gè)類在 MRO 中下一個(gè)直接父類, 你甚至可以在一個(gè)沒(méi)有直接父類的類中使用它。例如,考慮如下這個(gè)類:

class A:
    def spam(self):
        print('A.spam')
        super().spam()

如果你試著直接使用這個(gè)類就會(huì)出錯(cuò):

>>> 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'
>>>

但是,如果你使用多繼承的話看看會(huì)發(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() 實(shí)際上調(diào)用的是跟類 A 毫無(wú)關(guān)系的類 B 中的 spam() 方法。 這個(gè)用類 C 的 MRO 列表就可以完全解釋清楚了:

>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
<class 'object'>)
>>>

在定義混入類的時(shí)候這樣使用super() 是很普遍的??梢詤⒖?.13和8.18小節(jié)。

然而,由于super() 可能會(huì)調(diào)用不是你想要的方法,你應(yīng)該遵循一些通用原則。 首先,確保在繼承體系中所有相同名字的方法擁有可兼容的參數(shù)簽名(比如相同的參數(shù)個(gè)數(shù)和參數(shù)名稱)。 這樣可以確保 super() 調(diào)用一個(gè)非直接父類方法時(shí)不會(huì)出錯(cuò)。 其次,最好確保最頂層的類提供了這個(gè)方法的實(shí)現(xiàn),這樣的話在 MRO 上面的查找鏈肯定可以找到某個(gè)確定的方法。

在 Python 社區(qū)中對(duì)于super() 的使用有時(shí)候會(huì)引來(lái)一些爭(zhēng)議。 盡管如此,如果一切順利的話,你應(yīng)該在你最新代碼中使用它。 Raymond Hettinger 為此寫了一篇非常好的文章 “Python’s super() Considered Super!” , 通過(guò)大量的例子向我們解釋了為什么super() 是極好的。

子類中擴(kuò)展 property

問(wèn)題

在子類中,你想要擴(kuò)展定義在父類中的 property 的功能。

解決方案

考慮如下的代碼,它定義了一個(gè) 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")

下面是一個(gè)示例類,它繼承自 Person 并擴(kuò)展了 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)

接下來(lái)使用這個(gè)新類:

>>> 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
>>>

如果你僅僅只想擴(kuò)展 property 的某一個(gè)方法,那么可以像下面這樣寫:

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)

討論

在子類中擴(kuò)展一個(gè) property 可能會(huì)引起很多不易察覺(jué)的問(wèn)題, 因?yàn)橐粋€(gè) property 其實(shí)是getter、setterdeleter方法的集合,而不是單個(gè)方法。 因此,但你擴(kuò)展一個(gè)property的時(shí)候,你需要先確定你是否要重新定義所有的方法還是說(shuō)只修改其中某一個(gè)。

在第一個(gè)例子中,所有的 property 方法都被重新定義。 在每一個(gè)方法中,使用了 super()來(lái)調(diào)用父類的實(shí)現(xiàn)。 在 setter 函數(shù)中使用super(SubPerson, SubPerson).name.__set__(self, value)的語(yǔ)句是沒(méi)有錯(cuò)的。 為了委托給之前定義的 setter 方法,需要將控制權(quán)傳遞給之前定義的 name 屬性的__set__()方法。 不過(guò),獲取這個(gè)方法的唯一途徑是使用類變量而不是實(shí)例變量來(lái)訪問(wèn)它。 這也是為什么我們要使用 super(SubPerson, SubPerson) 的原因。

如果你只想重定義其中一個(gè)方法,那只使用 @property 本身是不夠的。比如,下面的代碼就無(wú)法工作:

class SubPerson(Person):
    @property  # Doesn't work
    def name(self):
        print('Getting name')
        return super().name

如果你試著運(yùn)行會(huì)發(fā)現(xiàn) setter 函數(shù)整個(gè)消失了:

>>> 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)該像之前說(shuō)過(guò)的那樣修改代碼:

class SubPerson(Person):
    @Person.getter
    def name(self):
        print('Getting name')
        return super().name

這么寫后,property 之前已經(jīng)定義過(guò)的方法會(huì)被復(fù)制過(guò)來(lái),而 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
>>>

在這個(gè)特別的解決方案中,我們沒(méi)辦法使用更加通用的方式去替換硬編碼的Person類名。 如果你不知道到底是哪個(gè)基類定義了 property, 那你只能通過(guò)重新定義所有 property 并使用 super()來(lái)將控制權(quán)傳遞給前面的實(shí)現(xiàn)。

值的注意的是上面演示的第一種技術(shù)還可以被用來(lái)擴(kuò)展一個(gè)描述器(在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)

最后值的注意的是,讀到這里時(shí),你應(yīng)該會(huì)發(fā)現(xiàn)子類化 setterdeleter方法其實(shí)是很簡(jiǎn)單的。 這里演示的解決方案同樣適用,但是在 [http://bugs.python.org/issue14965](Python 的 issue 頁(yè)面)報(bào)告的一個(gè) bug,或許會(huì)使得將來(lái)的 Python 版本中出現(xiàn)一個(gè)更加簡(jiǎn)潔的方法。

創(chuàng)建新的類或?qū)嵗龑傩?/h2>

問(wèn)題

你想創(chuàng)建一個(gè)新的擁有一些額外功能的實(shí)例屬性類型,比如類型檢查。

解決方案

如果你想創(chuàng)建一個(gè)全新的實(shí)例屬性,可以通過(guò)一個(gè)描述器類的形式來(lái)定義它的功能。下面是一個(gè)例子:

# 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]

一個(gè)描述器就是一個(gè)實(shí)現(xiàn)了三個(gè)核心的屬性訪問(wèn)操作(get, set, delete)的類, 分別為__get__() 、__set__()__delete__() 這三個(gè)特殊的方法。 這些方法接受一個(gè)實(shí)例作為輸入,之后相應(yīng)的操作實(shí)例底層的字典。

為了使用一個(gè)描述器,需將這個(gè)描述器的實(shí)例作為類屬性放到一個(gè)類的定義中。例如:

class Point:
    x = Integer('x')
    y = Integer('y')

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

當(dāng)你這樣做后,所有隊(duì)描述器屬性(比如 x 或 y)的訪問(wèn)會(huì)被 __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
>>>

作為輸入,描述器的每一個(gè)方法會(huì)接受一個(gè)操作實(shí)例。 為了實(shí)現(xiàn)請(qǐng)求操作,會(huì)相應(yīng)的操作實(shí)例底層的字典(dict屬性)。 描述器的 self.name 屬性存儲(chǔ)了在實(shí)例字典中被實(shí)際使用到的 key。

討論

描述器可實(shí)現(xiàn)大部分Python類特性中的底層魔法, 包括@classmethod@staticmethod 、@property ,甚至是__slots__特性。

通過(guò)定義一個(gè)描述器,你可以在底層捕獲核心的實(shí)例操作(get, set, delete),并且可完全自定義它們的行為。 這是一個(gè)強(qiáng)大的工具,有了它你可以實(shí)現(xiàn)很多高級(jí)功能,并且它也是很多高級(jí)庫(kù)和框架中的重要工具之一。

描述器的一個(gè)比較困惑的地方是它只能在類級(jí)別被定義,而不能為每個(gè)實(shí)例單獨(dú)定義。因此,下面的代碼是無(wú)法工作的:

# 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

同時(shí),__get__()方法實(shí)現(xiàn)起來(lái)比看上去要復(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__() 看上去有點(diǎn)復(fù)雜的原因歸結(jié)于實(shí)例變量和類變量的不同。 如果一個(gè)描述器被當(dāng)做一個(gè)類變量來(lái)訪問(wèn),那么 instance 參數(shù)被設(shè)置成 None 。 這種情況下,標(biāo)準(zhǔn)做法就是簡(jiǎn)單的返回這個(gè)描述器本身即可(盡管你還可以添加其他的自定義操作)。例如:

>>> 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>
>>>

描述器通常是那些使用到裝飾器或元類的大型框架中的一個(gè)組件。同時(shí)它們的使用也被隱藏在后面。 舉個(gè)例子,下面是一些更高級(jí)的基于描述器的代碼,并涉及到一個(gè)類裝飾器:

# 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

最后要指出的一點(diǎn)是,如果你只是想簡(jiǎn)單的自定義某個(gè)類的單個(gè)屬性訪問(wèn)的話就不用去寫描述器了。 這種情況下使用8.6小節(jié)介紹的 property 技術(shù)會(huì)更加容易。 當(dāng)程序中有很多重復(fù)代碼的時(shí)候描述器就很有用了 (比如你想在你代碼的很多地方使用描述器提供的功能或者將它作為一個(gè)函數(shù)庫(kù)特性)。

使用延遲計(jì)算屬性

問(wèn)題

你想將一個(gè)只讀屬性定義成一個(gè) property,并且只在訪問(wèn)的時(shí)候才會(huì)計(jì)算結(jié)果。 但是一旦被訪問(wèn)后,你希望結(jié)果值被緩存起來(lái),不用每次都去計(jì)算。

解決方案

定義一個(gè)延遲屬性的一種高效方法是通過(guò)使用一個(gè)描述器類,如下所示:

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

你需要像下面這樣在一個(gè)類中使用它:

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

下面在一個(gè)交互環(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
>>>

仔細(xì)觀察你會(huì)發(fā)現(xiàn)消息 Computing areaComputing perimeter 僅僅出現(xiàn)一次。

討論

很多時(shí)候,構(gòu)造一個(gè)延遲計(jì)算屬性的主要目的是為了提升性能。 例如,你可以避免計(jì)算這些屬性值,除非你真的需要它們。 這里演示的方案就是用來(lái)實(shí)現(xiàn)這樣的效果的, 只不過(guò)它是通過(guò)以非常高效的方式使用描述器的一個(gè)精妙特性來(lái)達(dá)到這種效果的。

正如在其他小節(jié)(如8.9小節(jié))所講的那樣,當(dāng)一個(gè)描述器被放入一個(gè)類的定義時(shí), 每次訪問(wèn)屬性時(shí)它的 __get__()、__set__()__delete__()方法就會(huì)被觸發(fā)。 不過(guò),如果一個(gè)描述器僅僅只定義了一個(gè)__get__() 方法的話,它比通常的具有更弱的綁定。 特別地,只有當(dāng)被訪問(wèn)屬性不在實(shí)例底層的字典中時(shí)__get__()方法才會(huì)被觸發(fā)。

lazyproperty類利用這一點(diǎn),使用 __get__() 方法在實(shí)例中存儲(chǔ)計(jì)算出來(lái)的值, 這個(gè)實(shí)例使用相同的名字作為它的 property。 這樣一來(lái),結(jié)果值被存儲(chǔ)在實(shí)例字典中并且以后就不需要再去計(jì)算這個(gè) property 了。 你可以嘗試更深入的例子來(lái)觀察結(jié)果:

>>> c = Circle(4.0)
>