摘要:許多程序員發現賦值語句比方法函數看起來更清晰。自從和屬性的創建來自,我們必須經常定義特性使用如下代碼這允許我們用一條簡單的語句添加一張牌到手中像下面這樣前面的賦值語句有一個缺點,因為它看起來像一張牌替代了所有的牌。
注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python
對象就是一些特性的集合,包括方法和屬性。object類的默認行為包括設置、獲取和刪除屬性。我們經常需要修改這些行為來改變一個對象的屬性。
本章將重點關注以下五個層次的字段訪問:
內置字段的處理,這是最簡單的,但最不精明的選擇。
回顧一下@property裝飾器。特性擴展了屬性的概念,把處理過程包含到了已定義的方法函數中。
如何利用低級別的特殊方法去控制屬性訪問方法:__getattr__()、__setattr__()和__delattr__()。這些特殊的方法允許我們構建更復雜的屬性處理。
了解__getattribute__()方法,它提供了更細粒度的屬性控制。這可以讓我們寫不尋常的屬性處理。
最后,我們將看看描述符。這些都是用來訪問一個屬性的,但它們涉及到更復雜的設計決策。在Python中大量使用描述符來實現特性、靜態方法和類方法。
在這一章,我們將會看到默認處理如何工作的細節。我們需要決定何時何地來覆寫默認行為。在某些情況下,我們希望我們的屬性不僅僅是實例化變量。在其他情況下,我們可能想要防止屬性的添加。我們的屬性可能有更復雜的行為。
同樣,在我們探索了解描述符時,我們將更深入的理解Python的內部是怎樣工作的。我們不需要經常顯式的使用描述符。我們經常隱式的使用它們,因為它們是實現Python一些特性的機制。
基本屬性處理默認情況下,我們創建的任何類對屬性都將允許以下四個行為:
通過設置值來創建一個新的屬性
給存在的屬性設置值
獲取屬性的值
刪除屬性
我們可以使用像下面代碼這樣簡單的表示。創建一個簡單的、通用的類和該類的一個對象:
>>> class Generic: ... pass ... >>> g = Generic()
前面的代碼允許我們創建、獲取、設置和刪除屬性。我們可以輕松地創建和獲取一個屬性。以下是一些示例:
>>> g.attribute = "value" >>> g.attribute "value" >>> g.unset Traceback (most recent call last): File "", line 1, in AttributeError: "Generic" object has no attribute "unset" >>> del g.attribute >>> g.attribute Traceback (most recent call last): File " ", line 1, in 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 "", line 1, in AttributeError: "namespace" object has no attribute "attribute"
我們可以為這個對象創建屬性。任何試圖使用未定義的屬性都會拋出異常。當我們創建一個object類實例時SimpleNamespace會有不同的行為。一個簡單的object類實例不允許創建新的屬性;它缺乏內部__dict__結構,Python會保存屬性和值到該結構里面。
1、屬性和__init__()方法大多數時候,我們使用類的__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
在每次需要總和的時候,通過重新掃描手中的牌延遲計算。這可是非常昂貴的開銷。
1、及早計算屬性以下是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方法函數看起來更清晰。
2、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__()做到的。
我們不會看所有這些選擇。相反,我們將關注兩個最常用的技術:擴展和包裝。我們將創建不可變對象,看看其他方法來及早計算特性值。
1、通過__slots__創建不可變對象如果我們不能夠設置一個屬性或創建一個新的,且對象是不可變的。則以下是我們希望在交互式Python中所能夠看到的:
>>> c = card21(1,"?") >>> c.rank = 12 Traceback (most recent call last): File "", line 1, in File " ", line 30, in __setattr__ TypeError: Cannot set rank >>> c.hack = 13 Traceback (most recent call last): File " ", line 1, in File " ", 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__特性的主要目的是通過限制字段的數量來節省內存。
2、創建不可變對象作為元組的子類我們也可以通過給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 "", line 1, in File " ", line 7, in __setattr__AttributeError
我們不能輕易的改變牌值。然而,我們仍然可以調整d.__dict__來引入額外的屬性。
有這必要嗎
也許,簡單的工作可以確保對象不是不小心誤用。實際上,我們對從異常得到的診斷信息和跟蹤,比我們在極其安全的不可變類中更感興趣。
3、及早計算屬性我們可以定義一個對象,它的屬性在設置值后盡可能快的及早計算。對象最優訪問就是進行一次計算結果多次使用。
我們能夠定義很多的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值。
我們可以設計一個模型,跟蹤設置變量的順序;這一模型可以節省我們在設置另一個變量重新計算相關結果之前清除一個變量。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/37636.html
摘要:不像其他屬性,描述符在類級別上創建。當所有者類被定義時,每個描述符對象都是被綁定到一個不同的類級別屬性的描述符類實例。這必須返回描述符的值。此外,描述符對有一個方便的響應和請求格式。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python __getattribute__()方法 __getattribute__()方法是...
摘要:第一是在對象生命周期中初始化是最重要的一步每個對象必須正確初始化后才能正常工作。第二是參數值可以有多種形式。基類對象的方法對象生命周期的基礎是它的創建初始化和銷毀。在某些情況下,這種默認行為是可以接受的。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python __init__()方法意義重大的原因有兩個。第一是在對象生命...
摘要:所以搞清楚是理解對象屬性描述符的唯一途徑。是一個對象,對象里的屬性描述符有兩種類型數據描述符和存取描述符。描述符必須是這兩種形式之一不能同時是兩者。描述符中未顯示設置的特性使用其默認值。創建一個新屬性默認描述符的鍵值都是或者。 對象屬性描述符 當別人對你提及對象屬性描述符,可能會蒙逼。而如果提及對象屬性的 get/set 方法就秒懂了,標準描述和習慣表述在這里有些差別,但是指向的是同一...
閱讀 3801·2021-11-24 09:39
閱讀 1810·2021-11-02 14:41
閱讀 814·2019-08-30 15:53
閱讀 3480·2019-08-29 12:43
閱讀 1189·2019-08-29 12:31
閱讀 3087·2019-08-26 13:50
閱讀 795·2019-08-26 13:45
閱讀 986·2019-08-26 10:56