注:原書做者 Steven F. Lott,原書名爲 Mastering Object-oriented Pythonpython
對象就是一些特性的集合,包括方法和屬性。object
類的默認行爲包括設置、獲取和刪除屬性。咱們常常須要修改這些行爲來改變一個對象的屬性。程序員
本章將重點關注如下五個層次的字段訪問:數據庫
內置字段的處理,這是最簡單的,但最不精明的選擇。設計模式
回顧一下@property
裝飾器。特性擴展了屬性的概念,把處理過程包含到了已定義的方法函數中。安全
如何利用低級別的特殊方法去控制屬性訪問方法:__getattr__()
、__setattr__()
和__delattr__()
。這些特殊的方法容許咱們構建更復雜的屬性處理。網絡
瞭解__getattribute__()
方法,它提供了更細粒度的屬性控制。這可讓咱們寫不尋常的屬性處理。app
最後,咱們將看看描述符。這些都是用來訪問一個屬性的,但它們涉及到更復雜的設計決策。在Python中大量使用描述符來實現特性、靜態方法和類方法。函數
在這一章,咱們將會看到默認處理如何工做的細節。咱們須要決定什麼時候何地來覆寫默認行爲。在某些狀況下,咱們但願咱們的屬性不只僅是實例化變量。在其餘狀況下,咱們可能想要防止屬性的添加。咱們的屬性可能有更復雜的行爲。ui
一樣,在咱們探索瞭解描述符時,咱們將更深刻的理解Python的內部是怎樣工做的。咱們不須要常常顯式的使用描述符。咱們常常隱式的使用它們,由於它們是實現Python一些特性的機制。this
默認狀況下,咱們建立的任何類對屬性都將容許如下四個行爲:
經過設置值來建立一個新的屬性
給存在的屬性設置值
獲取屬性的值
刪除屬性
咱們可使用像下面代碼這樣簡單的表示。建立一個簡單的、通用的類和該類的一個對象:
>>> class Generic: ... pass ... >>> g = Generic()
前面的代碼容許咱們建立、獲取、設置和刪除屬性。咱們能夠輕鬆地建立和獲取一個屬性。如下是一些示例:
>>> g.attribute = "value" >>> g.attribute 'value' >>> g.unset Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Generic' object has no attribute 'unset' >>> del g.attribute >>> g.attribute Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Generic' object has no attribute 'attribute'
咱們能夠添加、更改和刪除屬性。若是咱們試圖獲取一個未設置的屬性或刪除一個不存在的屬性時會發生異常。
稍微更好的方法就是使用types.SimpleNamespace
類的一個實例。設置特性是同樣的,可是咱們不須要建立額外的類定義。咱們建立一個SimpleNamespace
類對象來代替,以下:
>>> import types >>> n = types.SimpleNamespace()
在如下代碼中,咱們能夠看到爲SimpleNamespace
類工做的相同用例:
>>> n.attribute = "value" >>> n.attribute 'value' >>> del n.attribute >>> n.attribute Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'namespace' object has no attribute 'attribute'
咱們能夠爲這個對象建立屬性。任何試圖使用未定義的屬性都會拋出異常。當咱們建立一個object
類實例時SimpleNamespace
會有不一樣的行爲。一個簡單的object
類實例不容許建立新的屬性;它缺少內部__dict__
結構,Python會保存屬性和值到該結構裏面。
大多數時候,咱們使用類的__init__()
方法來建立一系列的初始屬性。理想狀況下,咱們爲__init__()
中全部屬性提供默認值。
不須要提供全部屬性到__init__()
方法。正由於如此,存在或不在的屬性能夠做爲一個對象狀態的一部分。
一個可選屬性能夠超越類定義的限制。對於一個類來講,有一組好的屬性定義意義甚大。經過建立一個子類或父類,屬性一般能夠更清晰地被添加(或刪除)。
所以,可選屬性意味着一種非正式的子類關係。所以,當咱們使用可選屬性時會碰到可憐的多態性。
思考一下21點遊戲,只有容許一次分牌。若是一手牌已經分牌,就不能再分牌。有幾種方法,咱們能夠模擬一下:
咱們能夠由Hand.split()
方法建立一個SplitHand
子類。在此咱們不詳細展現。
咱們能夠在Hand
對象中建立一個狀態屬性,由Hand.split()
方法建立。理想狀況下,這是一個布爾值,可是咱們能夠實現它做爲一個可選屬性。
下面是經過一個可選屬性檢測可分離和不可分離的Hand.split()
:
def split(self, deck): assert self.cards[0].rank == self.cards[1].rank try: self.split_count raise CannotResplit except AttributeError: h0 = Hand(self.dealer_card, self.cards[0], deck.pop()) h1 = Hand(self.dealer_card, self.cards[1], deck.pop()) h0.split_count = h1.split_count = 1 return h0, h1
實際上,split()
方法是檢測是否有split_count
屬性。若是有這個屬性,則是已經分牌的手牌且該方法拋出異常。若是split_count
屬性不存在,容許分牌。
一個可選屬性的優點是使__init__()
方法有相對整潔的狀態標識。劣勢是模糊了對象的狀態。使用try:
塊來肯定對象狀態可能會變得很是混亂,咱們應該避免。
特性是一個方法函數,它(語法上)看上去像一個簡單的屬性。咱們能夠獲取、設置和刪除特性值就像咱們如何獲取、設置和和刪除屬性值同樣。這裏有一個重要的區別,特性其實是一個函數且能夠處理,而不是簡單地保存一個引用到一個對象。
除了更加尖端以外,特性和屬性之間的另外一個差異就是咱們不能輕易將新特性附加到現有對象上;然而,默認狀況下咱們能夠地輕易給對象添加屬性。在這方面特性和簡單的屬性是不同的。
有兩種方法建立特性。咱們可使用@property
裝飾器或者咱們可使用property()
函數。純粹是語法的差別。咱們將更多的關注裝飾器。
咱們看看特性的兩個基本設計模式:
及早計算:在這個設計模式中,當咱們經過特性設置一個值時,其餘屬性也一樣計算。
延遲計算:在這個設計模式中,計算將被推遲直到須要的時候,經過特性。
爲了比較前兩種特性處理,咱們將分割Hand
對象的常見的特性到一個抽象父類,以下所示:
class Hand: def __str__(self): return ", ".join(map(str, self.card)) def __repr__(self): return "{__class__.__name__}({dealer_card!r}, {_cards_str})" .format(__class__=self.__class__, _cards_str=", " .join(map(repr, self.card)), **self.__dict__)
在前面的代碼中,咱們只是定義了一些字符串表示方法。
下面是Hand
的一個子類,total
是一個延遲屬性,只有在須要的時候進行計算:
class Hand_Lazy(Hand): def __init__(self, dealer_card, *cards): self.dealer_card = dealer_card self._cards = list(cards) @property def total(self): delta_soft = max(c.soft-c.hard for c in self._cards) hard_total = sum(c.hard for c in self._cards) if hard_total + delta_soft <= 21: return hard_total + delta_soft return hard_total @property def card(self): return self._cards @card.setter def card(self, aCard): self._cards.append(aCard) @card.deleter def card(self): self._cards.pop(-1)
Hand_Lazy
類初始化一個帶有一組Cards
對象的Hand
對象。total
特性是一個只有在須要的時候計算總和的方法。此外,咱們定義了一些其餘特性更新手中的牌。Card
屬性能夠獲取、設置或刪除手中的牌。咱們將在setter
和deleter
屬性章節看到這些。
咱們能夠建立一個Hand
對象,total
做爲一個簡單的屬性出現:
>>> d = Deck() >>> h = Hand_Lazy(d.pop(), d.pop(), d.pop()) >>> h.total 19 >>> h.card = d.pop() >>> h.total 29
在每次須要總和的時候,經過從新掃描手中的牌延遲計算。這但是很是昂貴的開銷。
如下是Hand
的一個子類,total
是一個簡單的屬性,它會在每張牌被添加後當即計算:
class Hand_Eager(Hand): def __init__(self, dealer_card, *cards): self.dealer_card = dealer_card self.total = 0 self._delta_soft = 0 self._hard_total = 0 self._cards = list() for c in cards: self.card = c @property def card(self): return self._cards @card.setter def card(self, aCard): self._cards.append(aCard) self._delta_soft = max(aCard.soft - aCard.hard, self._delta_soft) self._hard_total += aCard.hard self._set_total() @card.deleter def card(self): removed = self._cards.pop(-1) self._hard_total -= removed.hard # Issue: was this the only ace? self._delta_soft = max(c.soft - c.hard for c in self._cards) self._set_total() def _set_total(self): if self._hard_total+self._delta_soft <= 21: self.total = self._hard_total + self._delta_soft else: self.total = self._hard_total
在這種狀況下,每添加一張牌,total
屬性就會更新。
其餘Card
——deleter
特性——及早地更新total
屬性不管牌在什麼時候被刪除。咱們將在下一節詳細查看deleter
。
客戶端認爲這兩個子類之間的語法相同(Hand_Lazy()
和Hand_Eager()
)
d = Deck() h1 = Hand_Lazy(d.pop(), d.pop(), d.pop()) print(h1.total) h2 = Hand_Eager(d.pop(), d.pop(), d.pop()) print(h2.total)
在這兩種狀況下,客戶端軟件簡單的使用total
字段。
使用特性的優點是,當實現改變時語法沒有改變。咱們能夠作一個相似getter/setter
簡單要求的方法函數。然而,getter/setter
方法函數涉及到並無什麼用處的額外語法。如下是兩個例子,其中一個是使用setter
方法,另外一個是使用賦值運算符:
obj.set_something(value) obj.something = value
賦值運算符(=)的存在乎圖很簡單。許多程序員發現賦值語句比setter
方法函數看起來更清晰。
setter
和deleter
特性在前面的例子中,咱們定義了Card
特性來處理額外的牌到Hand
類對象。
自從setter
(和deleter
)屬性的建立來自getter
,咱們必須常常定義getter
特性使用以下代碼:
@property def card(self): return self._cards @card.setter def card(self, aCard): self._cards.append(aCard) @card.deleter def card(self): self._cards.pop(-1)
這容許咱們用一條簡單的語句添加一張牌到手中像下面這樣:
h.card = d.pop()
前面的賦值語句有一個缺點,由於它看起來像一張牌替代了全部的牌。另外一方面,它也有一個優點,由於它使用簡單賦值來更新一個可變對象的狀態。咱們可使用__iadd__()
特殊方法,這樣作更簡潔。但咱們會等到第七章《建立數字》引入其餘特殊方法。
咱們當前的例子,沒有使人信服的理由來使用deleter
特性。即便沒有一個使人信服的理由,仍是有一些deleter
用法。不管如何,咱們還能夠利用它來刪除最後一張處理過的牌。這能夠用做分牌過程的一部分。
咱們能夠思考一下如下版本的split()
,以下代碼顯示:
def split(self, deck): """Updates this hand and also returns the new hand.""" assert self._cards[0].rank == self._cards[1].rank c1 = self._cards[-1] del self.card self.card = deck.pop() h_new = self.__class__(self.dealer_card, c1, deck.pop()) return h_new
前面的方法更新給定的手牌並返回新的Hand
對象。下面是一個分牌的例子:
>>> d = Deck() >>> c = d.pop() >>> h = Hand_Lazy(d.pop(), c, c) # Force splittable hand >>> h2 = h.split(d) >>> print(h) 2♠, 10♠ >>> print(h2) 2♠, A♠
一旦咱們有兩張牌,咱們可使用split()
產生第二個手牌。一張牌從最初的手牌中被移除。
這個版本的split()
固然是可行的。然而,彷佛有所好轉的使用split()
方法返回兩個新的Hand
對象。這樣,舊的、預分牌的Hand
實例能夠用做收集統計數據。
咱們來看看這三個規範的訪問屬性的特殊方法:getattr()
、setattr()
和delattr()
。此外,咱們會知道__dir__()
方法會顯示屬性名稱。咱們推遲到下一節來介紹__getattribute__()
。
第一節默認行爲的展現以下:
__setattr__()
方法將建立並設置屬性。
__getattr__()
方法將作兩件事。首先,若是一個屬性已經有值,__getattr__()
不使用,只是返回屬性值。其次,若是屬性沒有值,那麼__getattr__()
會有機會返回有意義的值。若是沒有屬性,它必定會拋出一個AttributeError
異常。
__delattr__()
方法刪除一個屬性。
__dir__()
方法返回屬性名稱列表。
__getattr__()
方法函數在更大的處理過程當中只有一個步驟;只有當屬性是未知的纔會去使用。若是屬性是已知的,不使用這種方法。__setattr__()
和__delattr__()
方法沒有內置的處理。這些方法不與額外的處理過程進行交互。
對於控制屬性訪問咱們有許多設計可選。這根據咱們的三個基本設計來選擇是擴展、包裝或發明。選擇以下:
咱們能夠擴展一個類,經過重寫__setattr__()
和__delattr__()
使它幾乎不可變。咱們也能夠經過__slots__
替換內部的__dict__
。
咱們能夠包裝類和委託屬性訪問到即將包裝的對象(或複合對象)。這可能涉及到覆寫全部三種方法。
咱們能夠在一個類中實現類特性行爲。使用這些方法,咱們能夠確保全部屬性集中處理。
咱們能夠建立延遲屬性值儘管它的值在須要的時候沒有(或不能)計算。可能會有一個屬性沒有值,直到從文件、數據庫或網絡中讀取到。這對於__getattr__()
是經常使用用法。
咱們能夠有及早屬性,在其餘屬性中自動設置時建立一個屬性值。這是經過覆寫__setattr__()
作到的。
咱們不會看全部這些選擇。相反,咱們將關注兩個最經常使用的技術:擴展和包裝。咱們將建立不可變對象,看看其餘方法來及早計算特性值。
__slots__
建立不可變對象若是咱們不可以設置一個屬性或建立一個新的,且對象是不可變的。則如下是咱們但願在交互式Python中所可以看到的:
>>> c = card21(1,'♠') >>> c.rank = 12 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 30, in __setattr__ TypeError: Cannot set rank >>> c.hack = 13 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 31, in __setattr__ AttributeError: 'Ace21Card' has no attribute 'hack'
前面的代碼顯示,咱們是不容許改變這個對象的屬性或添加一個到這個對象種。
爲了讓此操做能夠順利工做咱們須要變化這個類定義中的兩個地方。咱們將忽略不少類,只關注三個特性,使一個對象不可變,以下所示:
class BlackJackCard: """Abstract Superclass""" __slots__ = ('rank', 'suit', 'hard', 'soft') def __init__(self, rank, suit, hard, soft): super().__setattr__('rank', rank) super().__setattr__('suit', suit) super().__setattr__('hard', hard) super().__setattr__('soft', soft) def __str__(self): return "{0.rank}{0.suit}".format(self) def __setattr__(self, name, value): raise AttributeError("'{__class__.__name__}' has no attribute '{name}'" .format(__class__ = self.__class__, name = name))
咱們作了三個重要的變更:
咱們設置__slots__
到只被容許的屬性。這個將關閉對象內部__dict__
的特性且容許限制屬性。
咱們定義的__setattr__()
會引起一個異常比不作任何事有用的多。
咱們定義__init__()
使用的超類版本的__setattr__()
這樣值就能夠正確設置,儘管這個類中缺乏了正常工做的__setattr__()
方法。
當心一些,若是這樣作咱們能夠繞過不變性特性。
object.__setattr__(c, 'bad', 5)
這給咱們帶來了一個問題。咱們如何防止「邪惡的」程序員繞過不變性特性?這個問題是愚蠢的。咱們並不能阻止邪惡的程序員。另外一個一樣愚蠢的問題是,爲何一些邪惡的程序員寫代碼來規避不變性?咱們並不能阻止邪惡的程序員作邪惡的事情。
若是這個虛構的程序員不喜歡類中的不變性,他們能夠修改類的定義來刪除從新定義的__setattr__()
。不可變對象的重點是保證__hash__()
返回一個一致的值,而不是阻止人們寫爛的代碼。
不要濫用__slots__
__slots__
特性的主要目的是經過限制字段的數量來節省內存。
咱們也能夠經過給Card
屬性一個元組子類並覆寫__getattr__()
來建立一個不可變對象。在這種狀況下,咱們將翻譯__getattr__(name)
請求爲self[index]
請求。在第六章《建立容器和集合》中咱們將看到,self[index]
是由__getitem__(index)
來實現的。
下面是內置tuple
類的一個小擴展:
class BlackJackCard2(tuple): def __new__(cls, rank, suit, hard, soft): return super().__new__(cls, (rank, suit, hard, soft)) def __getattr__(self, name): return self[{'rank':0, 'suit':1, 'hard':2 , 'soft':3}[name]] def __setattr__(self, name, value): raise AttributeError
在本例中,咱們只是簡單的拋出了AttributeError
異常而不是提供詳細的錯誤消息。
當咱們使用前面的代碼中,咱們看到如下交互:
>>> d = BlackJackCard2('A', '♠', 1, 11) >>> d.rank 'A' >>> d.suit '♠' >>> d.bad = 2 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 7, in __setattr__AttributeError
咱們不能輕易的改變牌值。然而,咱們仍然能夠調整d.__dict__
來引入額外的屬性。
有這必要嗎
也許,簡單的工做能夠確保對象不是不當心誤用。實際上,咱們對從異常獲得的診斷信息和跟蹤,比咱們在極其安全的不可變類中更感興趣。
咱們能夠定義一個對象,它的屬性在設置值後儘量快的及早計算。對象最優訪問就是進行一次計算結果屢次使用。
咱們可以定義不少的setter
特性來作這些。然而,過多的setter
特性,每一個屬性都計算,會使得計算變得冗長複雜。
咱們能夠集中式的進行屬性處理。在接下來的例子中,咱們將對其調整來擴展Python的內部dict
類型。擴展dict
的優勢是,它可以很好地處理字符串的format()
方法。同時,咱們沒必要過多擔憂設置額外的被忽略的屬性值。
咱們但願相似下面的代碼:
>>> RateTimeDistance(rate=5.2, time=9.5) {'distance': 49.4, 'time': 9.5, 'rate': 5.2} >>> RateTimeDistance(distance=48.5, rate=6.1) {'distance': 48.5, 'time': 7.950819672131148, 'rate': 6.1}
咱們能夠在RateTimeDistance
對象中設置值。額外的屬性能夠很輕鬆的被計算。咱們能夠一次性作到這些,以下代碼所示:
>>> rtd = RateTimeDistance() >>> rtd.time = 9.5 >>> rtd {'time': 9.5} >>> rtd.rate = 6.24 >>> rtd {'distance': 59.28, 'time': 9.5, 'rate': 6.24}
下面是內置dict
類型的擴展。咱們擴展了基本dict
映射用來實現計算缺失的屬性:
class RateTimeDistance(dict): def __init__(self, *args, **kw): super().__init__(*args, **kw) self._solve() def __getattr__(self, name): return self.get(name,None) def __setattr__(self, name, value): self[name] = value self._solve() def __dir__(self): return list(self.keys()) def _solve(self): if self.rate is not None and self.time is not None: self['distance'] = self.rate * self.time elif self.rate is not None and self.distance is not None: self['time'] = self.distance / self.rate elif self.time is not None and self.distance is not None: self['rate'] = self.distance / self.time
dict
類型使用__init__()
來填充內部字典,而後試圖解決當前數據太多的問題。它使用__setattr__()
來添加新項目到字典。它也試圖在每次設置值的時候解答等式。
在__getattr__()
中,在等式中咱們使用None
代表值的缺失。這容許咱們設置一個字段爲None
代表它是一個缺失的值,這將迫使爲此尋找解決方案。例如,咱們能夠基於用戶輸入或者一個網絡請求,全部參數被賦予一個值,但一個變量設置爲None
。
咱們能夠以下使用:
>>> rtd = RateTimeDistance(rate=6.3, time=8.25, distance=None) >>> print("Rate={rate}, Time={time}, Distance={distance}".format(**rtd)) Rate=6.3, Time=8.25, Distance=51.975
請注意,咱們不能輕易地在這個類裏面設置屬性值。
讓咱們考慮下面這行代碼:
self.distance = self.rate * self.time
若是咱們要編寫以前的代碼片斷,咱們會在__setattr__()
和_solve()
之間進行無限的遞歸調用。當咱們使用self['distance']
到這個例子中,咱們避免了遞歸調用__setattr__()
。
一樣重要的是要注意,一旦設置了全部三個值,該對象不能輕易被改變來提供新的解決方案。
咱們不能簡單地給rate
設置一個新值且計算time
新值必須讓distance
不變。爲了調整這個模型,咱們須要清除一個變量以及爲另外一個變量設置一個新值:
>>> rtd.time = None >>> rtd.rate = 6.1 >>> print("Rate={rate}, Time={time}, Distance={distance}".format(**rtd)) Rate=6.1, Time=8.25, Distance=50.324999999999996
這裏,咱們清除time
且改變rate
獲得一個新的解決方案來使用既定的distance
值。
咱們能夠設計一個模型,跟蹤設置變量的順序;這一模型能夠節省咱們在設置另外一個變量從新計算相關結果以前清除一個變量。