Python 屬性管理(整理轉自《Python學習手冊》)

插入在屬性訪問時運行的代碼

1.__getattr__ 和 __setattr__ 方法,把未定義的屬性獲取和所有的屬性賦值指向通用
的處理器方法。

2.__getattribute__ 方法,把所有屬性獲取都指向Python 2.6的新式類和Python 3.0的
所有類中的一個泛型處理器方法。

3.property內置函數,把特定屬性訪問定位到get和set處理器函數,也叫做特性
(Property)。

4.描述符協議,把特定屬性訪問定位到具有任意get和set處理器方法的類的實例。

正如我們將要看到的,所有4種技術在某種程度上具有同樣的目標,並且通常可能對於
給定的問題使用任何一種技術來編寫代碼。然而它們確實存在某些重要的不同。例如,
這裏列出的最後兩種技術適用於特定屬性,而前兩種則足夠通用,可以用於那些必須把
任意屬性指向包裝的對象的、基於委託的類。我們將會看到,所有4種方法在複雜性和
優雅性上也都有所不同,在使用中,我們必須通過實際應用來自行判斷。

特性

特性協議允許我們把一個特定屬性的get和set操作指向我們所提供的函數或方法,使得我
們能夠插入在屬性訪問的時候自動運行的代碼,攔截屬性刪除,並且如果願意的話,還
可爲屬性提供文檔。

通過 property 內置函數來創建特性並將其分配給類屬性,就像方法函數一樣。同樣,可
以通過子類和實例繼承屬性,就像任何其他類屬性一樣。它們的訪問攔截功能通過self
實例參數提供,該參數確保了在主體實例上訪問狀態信息和類屬性是可行的。

一個特性管理一個單個的、特定的屬性;儘管它不能廣泛地捕獲所有的屬性訪問,它允
許我們控制訪問和賦值操作,並且允許我們自由地把一個屬性從簡單的數據改變爲一個
計算,而不會影響已有的代碼。正如你將看到的,特性和描述符有很大的關係,它們基
本上是描述符的一種受限制的形式。

基礎知識
可以通過把一個內置函數的結果賦給一個類屬性來創建一個特性:
attribute = property(fget, fset, fdel, doc)

這個內置函數的參數都不是必需的,並且如果沒有傳遞參數的話,所有都取默認值
None 。這樣的操作是不受支持的,並且嘗試使用默認值將會引發一個異常。當使用它們
的時候,我們向 fget 傳遞一個函數來攔截屬性訪問,給 fset 傳遞一個函數進行賦值,並
且給 fdel 傳遞一個函數進行屬性刪除; doc 參數接收該屬性的一個文檔字符串,如果想
要的話(否則,該特性會賦值 fget 的文檔字符串,如果提供了 fget 的文檔字符串的話,
其默認值爲 None )。 fget 返回計算過的屬性值,並且 fset 和 fdel 不返回什麼(確實是
None )。

這個內置的函數調用返回一個特性對象,我們將它賦給了在類的作用域中要管理的屬性
的名稱,正是在類的作用域中每個實例都繼承了類。

第一個例子
爲了說明如何把這些轉換成有用的代碼,如下的類使用一個特性來記錄對一個名爲 name
的屬性的訪問,實際存儲的數據名爲 _name ,以便不會和特性搞混了:

class Person: # Use (object) in 2.6
    def __init__(self, name):
        self._name = name
    def getName(self):
        print('fetch...')
        return self._name
    def setName(self, value):
        print('change...')
        self._name = value
    def delName(self):
        print('remove...')
        del self._name
    name = property(getName, setName, delName, "name property docs")

bob = Person('Bob Smith') # bob has a managed attribute
print(bob.name) # Runs getName
bob.name = 'Robert Smith' # Runs setName
print(bob.name)
del bob.name # Runs delName
print('-'*20)
sue = Person('Sue Jones') # sue inherits property too
print(sue.name)
print(Person.name.__doc__)  # Or help(Person.name)

Output:

fetch...
Bob Smith
change...
fetch...
Robert Smith
remove...
fetch...
Sue Jones
name property docs

Python 2.6和Python 3.0中都可以使用特性,但是,它們要求在Python 2.6中派生一個新式
對象,才能使賦值正確地工作——爲了在Python 2.6中運行代碼,這裏把對象添加爲一個
超類(我們在Python 3.0中也可以使用超類,但是,這是暗含的,並且不是必需的)。
這個特定的特性所做的事情並不多——它只是攔截並跟蹤了一個屬性,這裏將它作爲展
示協議的一個例子。當這段代碼運行的時候,兩個實例繼承了該特性,就好像它們是附
加到其類的另外兩個屬性一樣。然而,捕獲了它們的屬性訪問:

In [1]: class Person:
   ...:     def __init__(self, name):
   ...:         self._name = name
   ...:     @property
   ...:     def name(self): # name = property(name)
   ...:         "name property docs"
   ...:         print('fetch...')
   ...:         return self._name
   ...:     @name.setter
   ...:     def name(self, value): # name = name.setter(name)
   ...:         print('change...')
   ...:         self._name = value
   ...:     @name.deleter
   ...:     def name(self): # name = name.deleter(name)
   ...:         print('remove...')
   ...:         del self._name
   ...:

In [2]: bob = Person('Bob Smith') # bob has a managed attribute
   ...: print(bob.name)  # Runs name getter (name 1)
   ...: bob.name = 'Robert Smith' # Runs name setter (name 2)
   ...: print(bob.name)
   ...: del bob.name # Runs name deleter (name 3)
   ...: print('-'*20)
   ...: sue = Person('Sue Jones') # sue inherits property too
   ...: print(sue.name)
   ...: print(Person.name.__doc__)
   ...:
fetch...
Bob Smith
change...
fetch...
Robert Smith
remove...
--------------------
fetch...
Sue Jones
name property docs

minghu6:真容易令人誤解迷惑, 此name非彼name,有點a(a=a)的感覺
和 property 手動賦值的結果相比,這個例子中,使用裝飾器來編寫特性只需要3行額外
的代碼(這是無法忽視的差別)。就像替代工具的通常情況一樣,在這兩種技術之間的
選擇很大程度上與個人愛好有關。

描述符

描述符提供了攔截屬性訪問的一種替代方法;它們與前面小節所討論的特性有很大的關
系。實際上,特性是描述符的一種——從技術上講, property 內置函數只是創建一個特
定類型的描述符的一種簡化方式,而這種描述符在屬性訪問時運行方法函數。

從功能上講,描述符協議允許我們把一個特定屬性的get和set操作指向我們提供的一個單
管理屬性

獨類對象的方法:它們提供了一種方式來插入在訪問屬性的時候自動運行的代碼,並且
它們允許我們攔截屬性刪除並且爲屬性提供文檔(如果願意的話)。

描述符作爲獨立的類創建,並且它們就像方法函數一樣分配給類屬性。和任何其他的類
屬性一樣,它們可以通過子類和實例繼承。通過爲描述符自身提供一個 self ,以及提供
客戶類的實例,都可以提供訪問攔截方法。因此,它們可以自己保留和使用狀態信息,
以及主體實例的狀態信息。例如,一個描述符可能調用客戶類上可用的方法,以及它所
定義的特定於描述符的方法。

和特性一樣,描述符也管理一個單個的、特定的屬性。儘管它不能廣泛地捕獲所有的屬
性訪問,但它提供了對獲取和賦值訪問的控制,並且允許我們自由地把簡單的數據修改
爲計算值從而改變一個屬性,而不會影響已有的代碼。特性實際上只是創建一種特定描
述符的方便方法,並且,正如我們所見到的,它們可以直接作爲描述符編寫。

然而,特性的應用領域相對狹窄,描述符提供了一種更爲通用的解決方案。例如,由於
它們編碼爲常規類,所以描述符擁有自己的狀態,可能參與描述符繼承層級,可以使用
複合來聚合對象,並且爲編寫內部方法和屬性文檔字符串提供一種自然的結構。
基礎知識
正如前面所提到的,描述符作爲單獨的類編寫,並且針對想要攔截的屬性訪問操作提供
特定命名的訪問器方法——當以相應的方式訪問分配給描述符類實例的屬性時,描述符
類中的獲取、設置和刪除等方法自動運行:

class Descriptor:
    "docstring goes here"
    def __get__(self, instance, owner): ... # Return attr value
    def __set__(self, instance, value): ... # Return nothing (None)
    def __delete__(self, instance): ... # Return nothing (None)

帶有任何這些方法的類都可以看作是描述符,並且當它們的一個實例分配給另一個類的
屬性的時候,它們的這些方法是特殊的——當訪問屬性的時候,會自動調用它們。如果
這些方法中的任何一個空缺,通常意味着不支持相應類型的訪問。然而,和特性不同,
省略一個 set 意味着允許這個名字在一個實例中重新定義,因此,隱藏了描述符——
要使得一個屬性是隻讀的,我們必須定義set來捕獲賦值並引發一個異常。
描述符方法參數
在進行任何真正的編程之前,先來回顧一些基礎知識。前面小節介紹的所有3種描
述符方法,都傳遞了描述符類實例(self )以及描述符實例所附加的客戶類的實例
instance )。

__get__訪問方法還額外地接收一個 owner 參數,指定了描述符實例要附加到的類。其
instance 參數要麼是訪問的屬性所屬的實例(用於instance.attr ),要麼當所訪問的
屬性直接屬於類的時候是None(用於class.attr)。前者通常針對實例訪問計算一個
值;如果描述符對象訪問是受支持的,後者通常返回 self

例如,在下面的例子中,當獲取 X.attr 的時候,Python自動運行 Descriptor 類的
__get__ 方法, Subject.attr 類屬性分配給該方法(和特性一樣,在Python 2.6中,要
在這裏使用描述符,我們必須派生自對象;在Python 3.0中,這是隱式的,但無傷大
雅):

>>> class Descriptor(object):
... def __get__(self, instance, owner):
... print(self, instance, owner, sep='\n')
...
>>> class Subject:
... attr = Descriptor() # Descriptor instance is class attr
...
>>> X = Subject()
>>> X.attr
<__main__.Descriptor object at 0x0281E690>
<__main__.Subject object at 0x028289B0>
<class '__main__.Subject'>
>>> Subject.attr
<__main__.Descriptor object at 0x0281E690>
None
<class '__main__.Subject'>

注意在第一個屬性獲取中自動傳遞到 __get__方法中的參數,當獲取X.attr的時候,就好
像發生瞭如下的轉換(儘管這裏的 Subject.attr 沒有再次調用 get ):
X.attr -> Descriptor.__get__(Subject.attr, X, Subject)
當描述符的實例參數爲 None 的時候,該描述符知道將直接訪問它。
只讀描述符
正如前面提到的,和特性不同,使用描述符直接忽略__set__ 方法不足以讓屬性成爲只讀
的,因爲描述符名稱可以賦給一個實例。在下面的例子中,對X.a的屬性賦值在實例對
象 X 中存儲了 a ,由此,隱藏了存儲在類 C 中的描述符:

>>> class D:
... def __get__(*args): print('get')
...
>>> class C:
... a = D()
...
管理屬性 | 947
>>> X = C()
>>> X.a # Runs inherited descriptor __get__
get
>>> C.a
get
>>> X.a = 99 # Stored on X, hiding C.a
>>> X.a
99
>>> list(X.__dict__.keys())
['a']
>>> Y = C()
>>> Y.a # Y still inherits descriptor
get
>>> C.a
get

這就是Python中所有實例屬性賦值工作的方式,並且它允許在它們的實例中類選擇性地
覆蓋類級默認值。要讓基於描述符的屬性成爲只讀的,捕獲描述符類中的賦值並引發一
個異常來阻止屬性賦值——當要賦值的屬性是一個描述符的時候,Python有效地繞過了
常規實例層級的賦值行爲,並且把操作指向描述符對象:

>>> class D:
... def __get__(*args): print('get')
... def __set__(*args): raise AttributeError('cannot set')
...
>>> class C:
... a = D()
...
>>> X = C()
>>> X.a # Routed to C.a.__get__
get
>>> X.a = 99 # Routed to C.a.__set__
AttributeError: cannot set

注意: 還要注意不要把描述符__delete__方法和通用的 __del__方法搞混淆了。調用前者是試圖
刪除所有者類的一個實例上的管理屬性名稱;後者是一種通用的實例析構器方法,當任
何類的一個實例將要進行垃圾回收的時候調用。 __delete__與我們將要在本章後面遇到的
__delattr__ 泛型屬性刪除方法關係更近。參見本書第29章瞭解關於操作符重載的更多內

特性和描述符是如何相關的

正如前面提到的,特性和描述符有很強的相關性—— property 內置函數只是創建描述符
的一種方便方式。既然已經知道了二者是如何工作的,我們應該能夠看到,可以使用如
下的一個描述符類來模擬 property 內置函數:

class Property:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel # Save unbound methods
        self.__doc__ = doc # or other callables
    def __get__(self, instance, instancetype=None):
        if instance is None:
            return self
        if self.fget is None:
            raise AttributeError("can't get attribute")
        return self.fget(instance)  # Pass instance to self

    # in property accessors
    def __set__(self, instance, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(instance, value)
    def __delete__(self, instance):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(instance)

class Person:
    def getName(self): ...
    def setName(self, value): ...
    name = Property(getName, setName) # Use like property()

這個 Property類捕獲了帶有描述符協議的屬性訪問,並且把請求定位到創建類的時候在
描述符狀態中傳入和保存的函數或方法。例如,屬性獲取從 Person 類指向 Property 類的
__get__方法,再回到 Person 類的 getName 。有了描述符,這“恰好可以工作”。
注意,儘管這個描述符類等同於只是處理基本的特性用法,使用 @decorator語法也只是
指定了設置和刪除操作,但是我們的 Property 類也必須用 setterdeleter 方法擴展,
這可能會節省裝飾的訪問器函數並且返回特性對象( self 應該足夠了)。既然 property
內置函數已經做了這些,這裏,我們將省略這一擴展的正式編碼。
還要注意,描述符用來實現Python的__slots__ ,使用存儲在類級別的描述符來截取slot
名稱,從而避免了實例屬性字典。

__getattr____getattribute__

到目前爲止,我們已經學習了特性和描述符——管理特定屬性的工具。__getattr__
__getattribute__操作符重載方法提供了攔截類實例的屬性獲取的另一種方法。就像特
性和描述符一樣,它們也允許我們插入當訪問屬性的時候自動運行的代碼。然而,我們
將會看到,這兩個方法有更廣泛的應用。

屬性獲取攔截表現爲兩種形式,可用兩個不同的方法來編寫:
__getattr__針對未定義的屬性運行——也就是說,屬性沒有存儲在實例上,或者沒
有從其類之一繼承。

__getattribute__ 針對每個屬性,因此,當使用它的時候,必須小心避免通過把屬
性訪問傳遞給超類而導致遞歸循環。

這兩個方法是一組屬性攔截方法的代表,這些方法還包括 __setattr____delattr__
由於這些方法具有相同的作用,我們在這裏將它們通常作爲一個單獨話題。

與特性和描述符不同,這些方法是Python的操作符重載協議的一部分——是類的特殊命
名的方法,由子類繼承,並且當在隱式的內置操作中使用實例的時候自動調用。和一個
類的所有方法一樣,它們每一個在調用的時候都接收第一個 self 參數,訪問任何請求的
實例狀態信息或該類的其他方法。
__getattr____getattribute__方法也比特性和描述符更加通用——它們可以用來攔截
對任何(幾乎所有的)實例屬性的獲取,而不僅僅只是分配給它們的那些特定名稱。因
此,這兩個方法很適合於通用的基於委託的編碼模式——它們可以用來實現包裝對象,
該對象管理對一個嵌套對象的所有屬性訪問。相反,我們必須爲想要攔截的每個屬性都
定義一個特性或描述符。

最後,這兩種方法比我們前面考慮的替代方法的應用領域更專注集中一些:它們只是
攔截屬性獲取,而不攔截屬性賦值。要捕獲賦值對屬性的更改,我們必須編寫一個
__setattr__方法——這是一個操作符重載方法,只對每個屬性獲取運行,必須小心避免
由於通過實例命名空間字典指向屬性賦值而導致的遞歸循環。

儘管很少用到,我們還是可以編寫一個 __delattr__ 重載方法(必須以同樣的方式避免循
環)來攔截屬性刪除。相反,特性和描述符通過設計捕獲訪問、設置和刪除操作。
基礎知識
__getattr____setattr__ 在本書第29章和第31章介紹,並且第31章簡單地提到了
__getattribute__ 。簡而言之,如果一個類定義了或繼承瞭如下方法,那麼當一個實例
用於後面的註釋所提到的情況時,它們將自動運行:

def __getattr__(self, name): # On undefined attribute fetch [obj.name]
def __getattribute__(self, name): # On all attribute fetch [obj.name]
def __setattr__(self, name, value): # On all attribute assignment [obj.name=value]
def __delattr__(self, name): # On all attribute deletion [del obj.name]

管理屬性
所有這些之中, self 通常是主體實例對象, name 是將要訪問的屬性的字符串名, value
是要賦給該屬性的對象。兩個get方法通常返回一個屬性的值,另兩個方法不返回什麼
( None )。例如,要捕獲每個屬性獲取,我們可以使用上面的前兩個方法;要捕獲屬性
賦值,可以使用第三個方法:

class Catcher:
    def __getattr__(self, name):
        print('Get:', name)
    def __setattr__(self, name, value):
        print('Set:', name, value)

X = Catcher()
X.job # Prints "Get: job"
X.pay # Prints "Get: pay"
X.pay = 99 # Prints "Set: pay 99"

這樣的代碼結構可以用來實現我們在委託設計模式。由於所有的屬性通常
都指向我們的攔截方法,所以我們可以驗證它們並將其傳遞到嵌入的、管理的對象中。
例如,下面的類跟蹤了對傳遞給包裝類的另一個對象的每一次屬性獲
取:

class Wrapper:
    def __init__(self, object):
        self.wrapped = object # Save object
    def __getattr__(self, attrname):
        print('Trace:', attrname) # Trace fetch
        return getattr(self.wrapped, attrname) # Delegate fetch

特性和描述符沒有這樣的類似功能,做不到對每個可能的包裝對象中每個可能的屬性編
寫訪問器。
避免屬性攔截方法中的循環
這些方法通常都容易使用,它們唯一複雜的部分就是潛在的循環(即遞歸)。由於
__getattr__僅針對未定義的屬性調用,所以它可以在自己的代碼中自由地獲取其他屬
性。然而,由於 __getattribute____setattr__ 針對所有的屬性運行,因此,它們的代
碼要注意在訪問其他屬性的時候避免再次調用自己並觸發一次遞歸循環。

例如,在一個__getattribute__ 方法代碼內部的另一次屬性獲取,將會再次觸發
__getattribute__ ,並且代碼將會循環直到內存耗盡:

def __getattribute__(self, name):
    x = self.other # LOOPS!

要解決這個問題,把獲取指向一個更高的超類,而不是跳過這個層級的版本—— object
類總是一個超類,並且它在這裏可以很好地起作用:

def __getattribute__(self, name):
    x = object.__getattribute__(self, 'other') # Force higher to avoid me

對於__setattr__,情況是類似的。在這個方法內賦值任何屬性,都會再次觸發
__setattr__並創建一個類似的循環:

def __setattr__(self, name, value):
    self.other = value # LOOPS!

要解決這個問題,把屬性作爲實例的 __dict__命名空間字典中的一個鍵賦值。這樣就避
免了直接的屬性賦值:

def __setattr__(self, name, value):
    self.__dict__['other'] = value  # Use atttr dict to avoid me

儘管這種方法比較少用到,但 __setattr__ 也可以把自己的屬性賦值傳遞給一個更高的超
類而避免循環,就像__getattribute__ 一樣:

def __setattr__(self, name, value):
    object.__setattr__(self, 'other', value) # Force higher to avoid me

相反,我們不能使用 dict 技巧在 getattribute 中避免循環:

def __getattribute__(self, name):
    x = self.__dict__['other'] # LOOPS!

獲取 dict 屬性本身會再次觸發 getattribute ,導致一個遞歸循環。很奇怪(因爲_setattr_並不是這樣),但
確實如此
__delattr__ 方法實際中很少用到,但是,當用到的時候,它針對每次屬性刪除而調用
(就像針對每次屬性賦值調用 __setattr__ 一樣)。因此,我們必須小心,在刪除屬性的
時候要避免循環,通過使用同樣的技術:命名空間字典或者超類方法調用。

__getattr____getattribute__比較
爲了概括 __getattr____getattribute__ 之間的編碼區別,下面的例子使用了這兩者來
實現3個屬性—— attr1 是一個類屬性, attr2 是一個實例屬性, attr3 是一個虛擬的管理
屬性,當獲取時計算它:

class GetAttr:
    attr1 = 1
    def __init__(self):
        self.attr2 = 2
    def __getattr__(self, attr): # On undefined attrs only
        print('get: ' + attr) # Not attr1: inherited from class
        return 3 # Not attr2: stored on instance

X = GetAttr()
print(X.attr1)
print(X.attr2)
print(X.attr3)
print('-'*40)

class GetAttribute(object): # (object) needed in 2.6 only
    attr1 = 1
    def __init__(self):
        self.attr2 = 2
    def __getattribute__(self, attr): # On all attr fetches
        print('get: ' + attr) # Use superclass to avoid looping here
        if attr == 'attr3':
            return 3
        else:
            return object.__getattribute__(self, attr)

X = GetAttribute()
print(X.attr1)
print(X.attr2)
print(X.attr3)

運行時, __getattr__版本攔截對 attr3 的訪問,因爲它是未定義的。另一方面,
__getattribute__版本攔截所有的屬性獲取,並且必須將那些沒有管理的屬性訪問指向
超類獲取器以避免循環:

1
2
get: attr3
3
----------------------------------------
get: attr1
1
get: attr2
2
get: attr3
3

儘管__getattribute__可以捕獲比__getattr__更多的屬性獲取,但是實際上,它們只是
一個主題的不同變體——如果屬性沒有物理地存儲,二者具有相同的效果。

管理技術比較

爲了概括我們在本章介紹的4種屬性管理方法之間的編碼區別,讓我們快速地來看看使
用每種技術的一個更全面的計算屬性的示例。如下的版本使用特性來攔截並計算名爲
square 和 cube 的屬性。注意它們的基本值是如何存儲到以下劃線開頭的名稱中的,因
此,它們不會與特性本身的名稱衝突:

#dynamically computed attributes with properties

管理屬性(Property)

class Powers:
    def __init__(self, square, cube):
        self._square = square # _square is the base value
        self._cube = cube # square is the property name

    def getSquare(self):
        return self._square ** 2

    def setSquare(self, value):
        self._square = value

    square = property(getSquare, setSquare)

    def getCube(self):
        return self._cube ** 3

    cube = property(getCube)

X = Powers(3, 4)
print(X.square) # 3 ** 2 = 9
print(X.cube) # 4 ** 3 = 64
X.square = 5
print(X.square) # 5 ** 2 = 25

要用描述符做到同樣的事情,我們用完整的類定義了屬性。注意,描述符把基礎值存儲
爲實例狀態,因此,它們必須再次使用下劃線開頭,以便不會與描述符的名稱衝突(正
如我們將在本章最後的示例中見到的,我們可以通過把基礎值存儲爲描述符狀態,從而
避免必須重新命名):

* Same, but with descriptors*

class DescSquare:
    def __get__(self, instance, owner):
        return instance._square ** 2
    def __set__(self, instance, value):
        instance._square = value

class DescCube:
    def __get__(self, instance, owner):
        return instance._cube ** 3

class Powers: # Use (object) in 2.6
        square = DescSquare()
        cube = DescCube()
    def __init__(self, square, cube):
        self._square = square # "self.square = square" works too,

X = Powers(3, 4)
print(X.square) # 3 ** 2 = 9
print(X.cube) # 4 ** 3 = 64
X.square = 5
print(X.square) # 5 ** 2 = 25

要使用 __getattr__訪問攔截來實現同樣的結果,我們再次用下劃線開頭的名稱存儲基礎
值,這樣對被管理的名稱訪問是未定義的,並且由此調用我們的方法。我們還需要編寫
一個 __setattrr__ 來攔截賦值,並且注意避免其潛在的循環:

Same, but with generic __getattr__ undefined attribute interception

class Powers:
    def __init__(self, square, cube):
        self._square = square
        self._cube = cube

    def __getattr__(self, name):
        if name == 'square':
            return self._square ** 2
        elif name == 'cube':
            return self._cube ** 3
        else:
            raise TypeError('unknown attr:' + name)

    def __setattr__(self, name, value):
        if name == 'square':
            self.__dict__['_square'] = value
        else:
            self.__dict__[name] = value

X = Powers(3, 4)
print(X.square) # 3 ** 2 = 9
print(X.cube) # 4 ** 3 = 64
X.square = 5
print(X.square) # 5 ** 2 = 25

最後一個選項,使用__getattribute__來編寫,類似於前一個版本。由於我們現在捕獲
了每一個屬性,因此必須把基礎值獲取指向超類以避免循環:

Same, but with generic _getattribute_ all attribute interception

class Powers:
    def __init__(self, square, cube):
        self._square = square
        self._cube = cube

    def __getattribute__(self, name):
        if name == 'square':
            return object.__getattribute__(self, '_square') ** 2
        elif name == 'cube':
            return object.__getattribute__(self, '_cube') ** 3
        else:
            return object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        if name == 'square':
            self.__dict__['_square'] = value
        else:
            self.__dict__[name] = value

    X = Powers(3, 4)
    print(X.square) # 3 ** 2 = 9
    print(X.cube) # 4 ** 3 = 64
    X.square = 5

管理屬性
print(X.square) # 5 ** 2 = 25
正如你所見到的,每種技術的編碼形式都有所不同,但是,所有4種方法在運行的時候
都產生同樣的結果:

9
64
25

要了解如何比較這些替代方案以及其他編碼選項的更多內容,在本章後面“示例:屬性
驗證”節的屬性驗證示例中,我們會更多地嘗試它們的實際應用。在此之前,我們需要
先學習和這些工具中的兩種相關的一個缺點。
攔截內置操作屬性

在介紹 __getattr____getattribute__ 的時候,我說它們分別攔截未定義的以及所有的
屬性獲取,這使得它們很適合用於基於委託的編碼模式。儘管對於常規命名的屬性來說
是這樣,但它們的行爲需要一些額外的澄清:對於隱式地使用內置操作獲取的方法名屬
性,這些方法可能根本不會運行。這意味着操作符重載方法調用不能委託給被包裝的對
象,除非包裝類自己重新定義這些方法。

例如,針對 __str____add____getitem__ 方法的屬性獲取分別通過打印、+表達式和
索引隱式地運行,而不會指向Python 3.0中的類屬性攔截方法。特別是:
在Python 3.0中, __getattr____getattribute__都不會針對這樣的屬性而運行。
在Python 2.6中,如果屬性在類中未定義的話, __getattr__會針對這樣的屬性運
行。
在Python 2.6中,__getattribute__只對於新式類可用,並且在Python 3.0中也可以

##########################################################################
##最後的事例
##########################################################################

示例:屬性驗證

爲了結束本章的內容,讓我們來看一個更實際的示例,以所有的4種屬性管理方案來編
寫代碼。我們將要使用的這個示例定義了一個 CardHolder 對象,它帶有4個屬性,其中3
個屬性是要管理的。管理的屬性在獲取或存儲的時候要驗證或轉換值。對於同樣的測試
代碼,所有4個版本都產生同樣的結果,但是,它們以不同的方式實現了它們的屬性。
這個示例包含了很大一部分需要自學的內容。然而我們不會詳細介紹其代碼,因爲它們
都使用了我們在本章中已經介紹過的概念。

使用特性來驗證
我們的第一段代碼使用了特性來管理3個屬性。與通常一樣,我們可以使用簡單的方法
而不是管理屬性,但是,如果我們在已有的代碼中已經使用了屬性,特性就能幫忙了。
特性根據屬性訪問自動運行代碼,但是關注屬性的一個特定集合,它們不會用來廣泛地
攔截所有屬性。

要理解這段代碼,關鍵是要注意到,__init__構造函數方法內部的屬性賦值也觸發了特
性的 setter 方法。例如,當這個方法分配給 self.name 時,它自動調用 setName 方法,該
方法轉換值並將其賦給一個叫做 __name 的實例屬性,以便它不會與特性的名稱衝突。
這一重命名(有時候叫做名稱壓縮)是必要的,因爲特性使用公用的實例狀態並且沒有
自己的實例狀態。存儲在一個屬性中的數據叫做 __name ,而叫做 name 的屬性總是特性,
而非數據。

最後,這個類管理了叫做 name 、 age 和 acct 的屬性;允許直接訪問屬性 addr ,並且提供
了一個名爲 remain 的只讀屬性,該屬性完全是虛擬的並且根據需要計算。爲了進行比
較,這個基於特性的程序包含了39行代碼:

class CardHolder:
    acctlen = 8 # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct # Instance data
        self.name = name # These trigger prop setters too
        self.age = age # __X mangled to have class name
        self.addr = addr # addr is not managed

    def getName(self):
        return self.__name

    def setName(self, value):
        value = value.lower().replace(' ', '_')
        self.__name = value

    name = property(getName, setName)
    def getAge(self):
        return self.__age

    def setAge(self, value):
        if value < 0 or value > 150:
            raise ValueError('invalid age')
        else:
            self.__age = value
            age = property(getAge, setAge)

    def getAcct(self):
        return self.__acct[:-3] + '***'

    def setAcct(self, value):
        value = value.replace('-', '')
        if len(value) != self.acctlen:
            raise TypeError('invald acct number')
        else:
            self.__acct = value

    acct = property(getAcct, setAcct)
    def remainGet(self): # Could be a method, not attr
        return self.retireage - self.age # Unless already using as attr

    remain = property(remainGet)

self測試代碼
如下的代碼測試我們的類;將這段代碼添加到文件的底部,或者把類放到一個模塊中並
先導入它。我們將對這個例子的所有4個版本使用這段同樣的測試代碼。當它運行的時
候,我們創建了管理的屬性類的兩個實例,並且獲取和修改其各種屬性。期待失效的操
作包裝在 try 語句中:

bob = CardHolder('1234-5678', 'Bob Smith', 40, '123 main st')
print(bob.acct, bob.name, bob.age, bob.remain, bob.addr, sep=' / ')

bob.name = 'Bob Q. Smith'
bob.age = 50
bob.acct = '23-45-67-89'
print(bob.acct, bob.name, bob.age, bob.remain, bob.addr, sep=' / ')

sue = CardHolder('5678-12-34', 'Sue Jones', 35, '124 main st')
print(sue.acct, sue.name, sue.age, sue.remain, sue.addr, sep=' / ')

try:
    sue.age = 200
except:
print('Bad age for Sue')
try:
    sue.remain = 5
except:
    print("Can't set sue.remain")
try:
    sue.acct = '1234567'
except:
    print('Bad acct for Sue')

如下是我們的self測試代碼的輸出。再一次說明,這對這個示例的所有版本都是一樣
的。分析這段代碼,看看類的方法是如何調用的。賬戶顯示出來,其中一些數字隱藏
了,名稱轉換爲一種標準格式,並且使用一個類屬性訪問攔截的時候,截止到退休的剩
餘時間就計算了出來:

12345*** / bob_smith / 40 / 19.5 / 123 main st
23456*** / bob_q._smith / 50 / 9.5 / 123 main st
56781*** / sue_jones / 35 / 24.5 / 124 main st
Bad age for Sue
Can't set sue.remain
Bad acct for Sue

使用描述符驗證

現在,讓我們使用描述符而不是使用特性來重新編寫示例。正如我們已經看到的,描述
符在功能和角色上與特性類似。實際上,特性基本上是描述符的一種受限制的形式。和
特性一樣,描述符設計來處理特定的屬性訪問,而不是通用的屬性訪問。和特性不同,
描述符有自己的狀態,並且它們是一種更爲通用的方案。

要理解這段代碼,注意 __init__ 構造函數方法內部的屬性賦值觸發了描述符的 __set__
作符方法,這一點還是很重要。例如,當構造函數方法分配給 self.name 時,它自動調
Name.__set__() 方法,該方法轉換值,並且將其賦給了叫做 name 的一個描述符屬性。
和前面基於特性的變體不同,在這個例子中,實際的 name 值附加到了描述符對象,而不
是客戶類實例。儘管我們可以把這個值存儲在實例狀態或描述符狀態中,後者避免了需
要用下劃線壓縮名稱以避免衝突。在 CardHolder 客戶類中,名爲 name 的屬性總是一個描
述符對象,而不是數據。

最後,這個類實現了和前面的版本同樣的屬性:它管理名爲 name 、 age 和 acct 的屬性。
允許直接訪問屬性 addr ,並且提供一個名爲 remain 的只讀屬性, remain 是完全虛擬的並
且根據需要計算。注意我們爲何在其描述符中捕獲對 remain 的賦值,並引發一個異常。
正如我們前面所介紹的,如果沒有這麼做,對一個實例這一屬性的賦值,將會默默地創
建一個實例屬性而隱藏了類屬性描述符。爲了進行比較,這個基於描述符的代碼佔了45
行:

class CardHolder:

    acctlen = 8 # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct # Instance data
        self.name = name # These trigger __set__ calls too
        self.age = age # __X not needed: in descriptor
        self.addr = addr # addr is not managed

class Name:
    def __get__(self, instance, owner): # Class names: CardHolder locals
        return self.name

    def __set__(self, instance, value):
        value = value.lower().replace(' ', '_')
        self.name = value

    name = Name()

class Age:
    def __get__(self, instance, owner):
        return self.age # Use descriptor data

    def __set__(self, instance, value):
        if value < 0 or value > 150:
            raise ValueError('invalid age')
        else:
            self.age = value

    age = Age()

class Acct:
    def __get__(self, instance, owner):
        return self.acct[:-3] + '***'

    def __set__(self, instance, value):
        value = value.replace('-', '')
        if len(value) != instance.acctlen: # Use instance class data
            raise TypeError('invald acct number')
        else:
            self.acct = value

    acct = Acct()

class Remain:
    def __get__(self, instance, owner):
        return instance.retireage - instance.age  # Triggers Age.__get__
    def __set__(self, instance, value):
        raise TypeError('cannot set remain')  # Else set allowed here

    remain = Remain()

使用__getattr__來驗證

正如我們已經見到的,__getattr__ 方法攔截所有未定義的屬性,因此,它可能比使用特
性或描述符更爲通用。在我們的例子中,當獲取一個管理的屬性的時候,我們通過直接
測試屬性名來獲知。其他的屬性物理地存儲在實例中,因而無法達到__getattr__。儘管
這種方法比使用特性或描述符更爲通用,但需要額外的工作來模擬專門關注屬性的其他
工具。我們需要在運行時檢查名稱,並且必須編寫一個__setattr__ 以攔截並驗證屬性賦
值。

對於這個例子的特性和描述符版本,注意 __init__ 構造函數方法中的屬性賦值觸發了類
__setattr__ 方法,這還是很關鍵的。例如,當這個方法分配給 self.name 時,它自動
地調用 setattr 方法,該方法轉換值,並將其分配給一個名爲 name 的實例屬性。通過
在該實例上存儲 name ,它確保了未來的訪問不會觸發__getattr__ 。相反, acct 存儲爲
_acct ,因此隨後對 acct 的訪問會調用 __getattr__

最後,像前兩個例子中的情況一樣,這個類管理名爲 name 、 age 和 acct 的屬性。允許直
接訪問屬性 addr ;並且提供一個名爲 remain 的只讀屬性,它是完全虛擬的並且根據需要
計算。

爲了進行比較,這個替代方法有32行代碼——比基於特性的版本少了7行,比使用描述符
的版本少了13行。當然,清晰與否比代碼大小更重要,但額外的代碼有時候意味着額外
的開發和維護工作。可能這裏更重要的是角色:像__getattr__這樣的通用工具可能更適
合於通用委託,而特性和描述符更直接是爲了管理特定屬性而設計。

還要注意,當設置未管理的屬性(例如, addr )的時候,這裏的代碼引發額外調用,然
而獲取未管理的屬性並不會引發額外調用,因爲它們是定義了的。儘管這可能對大多數
程序都會導致不可忽視的額外開銷,但只有當訪問管理的屬性的時候,特性和描述符才
會引發額外調用。

下面是代碼的__getattr__ 版本:

class CardHolder:

    acctlen = 8 # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct # Instance data
        self.name = name # These trigger __setattr__ too
        self.age = age # _acct not mangled: name tested
        self.addr = addr # addr is not managed


    def __getattr__(self, name):
        if name == 'acct': # On undefined attr fetches
            return self._acct[:-3] + '***' # name, age, addr are defined
        elif name == 'remain':
            return self.retireage - self.age # Doesn't trigger __getattr__
        else:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        if name == 'name': # On all attr assignments
            value = value.lower().replace(' ', '_') # addr stored directly
        elif name == 'age': # acct mangled to _acct
            if value < 0 or value > 150:
                raise ValueError('invalid age')
        elif name == 'acct':
            name = '_acct'
            value = value.replace('-', '')
            if len(value) != self.acctlen:
                raise TypeError('invald acct number')
        elif name == 'remain':
            raise TypeError('cannot set remain')

        self.__dict__[name] = value # Avoid looping

使用__getattribute__驗證

最後的變體使用__getattribute__在需要的時候攔截屬性獲取並管理它們。這裏,每
次屬性獲取都會捕獲,因此,我們測試屬性名稱來檢測管理的屬性,並將所有其他的
屬性指向超類以實現常規的獲取過程。這個版本和前面的版本一樣,使用了同樣的
__setattr__來捕獲賦值。

這段代碼的工作和__getattr__ 版本很相似,因此,我不想在這裏重複整個介紹。然而,
注意,由於每個屬性獲取都指向了 __getattribute__,所以這裏我們不需要壓縮名稱以
攔截它們( acct 存儲爲 acct )。另一方面,這段代碼必須負責把未壓縮的屬性獲取指向
一個超類以避免循環。

還要注意,對於設置和獲取未管理的屬性(例如, addr ),這個版本都會引發額外調
用。如果速度極爲重要,這個替代方法可能會是所有方案中最慢的。爲了進行比較,這
個版本也有32行代碼,和前面的版本一樣:

class CardHolder:
    acctlen = 8 # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct # Instance data
        self.name = name # These trigger __setattr__ too
        self.age = age # acct not mangled: name tested
        self.addr = addr # addr is not managed

    def __getattribute__(self, name):
        superget = object.__getattribute__ # Don't loop: one level up
        if name == 'acct': # On all attr fetches
            return superget(self, 'acct')[:-3] + '***'
        elif name == 'remain':
            return superget(self, 'retireage') - superget(self, 'age')
        else:
            return superget(self, name) # name, age, addr: stored

    def __setattr__(self, name, value):
        if name == 'name': # On all attr assignments
            value = value.lower().replace(' ', '_') # addr stored directly
        elif name == 'age':
            if value < 0 or value > 150:
                raise ValueError('invalid age')
        elif name == 'acct':
            value = value.replace('-', '')
            if len(value) != self.acctlen:
                raise TypeError('invald acct number')
        elif name == 'remain':
            raise TypeError('cannot set remain')

    self.__dict__[name] = value # Avoid loops, orig names
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章