簡介
Python 2.2 引進了 Python 描述符,同時還引進了一些新的樣式類,但是它們並沒有得到廣泛使用。Python 描述符是一種建立託管屬性的方法。除了其他優點外,託管屬性還用於保護屬性不受修改,或自動更新某個依賴屬性的值。
描述符增加了對 Python 的理解,改善了編碼技能。本文介紹了描述符協議,並演示瞭如何建立和使用描述符。
描述符協議
Python 描述符協議 只是一種在模型中引用屬性時指定將要發生事件的方法。它允許程式設計人員輕鬆、有效地管理屬性訪問:
- set
- get
- delete
在其他程式語言中,描述符被稱作 setter 和 getter,而公共函式用於獲得 (Get) 和設定 (Set) 一個私有變數。Python 沒有私有變數的概念,而描述符協議可以作為一種 Python 的方式來實現與私有變數類似的功能。
總的來說,描述符就是一個具有繫結行為的物件屬性,其屬性訪問將由描述符協議中的方法覆蓋。這些方法為 __get__
、__set__
和 __delete__
。如果這些方法中的任何一個針對某個物件定義,那麼它就被認為是一個描述符。通過 清單 1 進一步瞭解這些方法。
清單 1. 描述符方法
1 2 3 |
__get__(self, instance, owner) __set__(self, instance, value) __delete__(self, instance) |
其中:
__get__
用於訪問屬性。它返回屬性的值,或者在所請求的屬性不存在的情況下出現 AttributeError
異常。
__set__
將在屬性分配操作中呼叫。不會返回任何內容。
__delete__
控制刪除操作。不會返回內容。
需要注意,描述符被分配給一個類,而不是例項。修改此類,會覆蓋或刪除描述符本身,而不是觸發它的程式碼。
需要使用描述符的情況
考慮 email
屬性。在向該屬性分配值之前,需要對郵件格式進行檢驗。該描述符允許通過一個正規表示式處理電子郵件,然後對格式進行檢驗後將它分配給一個屬性。
在其他許多情況下,Python 協議描述符控制對屬性的訪問,如保護 name
屬性。
建立描述符
您可以通過許多方式建立描述符:
- 建立一個類並覆蓋任意一個描述符方法:
__set__
、__ get__
和__delete__
。當需要某個描述符跨多個不同的類和屬性,例如型別驗證,則使用該方法。 - 使用屬性型別,這種方法可以更加簡單、靈活地建立描述符。
- 使用屬性描述符,它結合了屬性型別方法和 Python 描述符。
以下示例在其操作方面均相似。不同之處在於實現方法。
使用類方法建立描述符
清單 2 演示在 Python 中控制屬性分配非常簡單。
清單 2. 使用類方法建立描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Descriptor(object): def __init__(self): self._name = '' def __get__(self, instance, owner): print "Getting: %s" % self._name return self._name def __set__(self, instance, name): print "Setting: %s" % name self._name = name.title() def __delete__(self, instance): print "Deleting: %s" %self._name del self._name class Person(object): name = Descriptor() |
使用這些程式碼並檢視輸出:
1 2 3 4 5 6 7 8 |
>>> user = Person() >>> user.name = 'john smith' Setting: john smith >>> user.name Getting: John Smith 'John Smith' >>> del user.name Deleting: John Smith |
通過以下方法覆蓋父類的 __set__()
、__get__()
和 __delete__()
方法,建立一個描述符類:
get
將輸出 Gettingdelete
將輸出 Deletingset
將輸出 Setting
並在分配之前將屬性值修改為標題(第一個字母大寫,其他字母為小寫)。這樣做有助於儲存和輸出名稱。
大寫轉換同樣可以移動到 __get__()
方法。_value
有一個初始值,並根據 get
請求轉換為標題。
使用屬性型別建立描述符
雖然 清單 2 中定義的描述符是有效的且可以正常使用,但是還可以使用屬性型別的方法。通過使用 property(),可以輕鬆地為任意屬性建立可用的描述符。建立 property()
的語法是 property(fget=None, fset=None, fdel=None, doc=None)
,其中:
- fget:屬性獲取方法
- fset:屬性設定方法
- fdel:屬性刪除方法
- doc:docstring
使用屬性重寫該例子,如 清單 3 所示。
清單 3. 使用屬性型別建立描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Person(object): def __init__(self): self._name = '' def fget(self): print "Getting: %s" % self._name return self._name def fset(self, value): print "Setting: %s" % value self._name = value.title() def fdel(self): print "Deleting: %s" %self._name del self._name name = property(fget, fset, fdel, "I'm the property.") |
使用該程式碼並檢視輸出:
1 2 3 4 5 6 7 8 |
>>> user = Person() >>> user.name = 'john smith' Setting: john smith >>> user.name Getting: John Smith 'John Smith' >>> del user.name Deleting: John Smith |
顯然,結果是相同的。注意,fget
、fset
和 fdel
方法是可選的,但是如果沒有指定這些方法,那麼將在嘗試各個操作時出現一個 AttributeError
異常。例如,宣告 name
屬性時,fset
被設定為 None
,然後開發人員嘗試向 name
屬性分配值。這時將出現一個 AttributeError
異常。
這種方法可以用於定義系統中的只讀屬性。
1 2 |
name = property(fget, None, fdel, "I'm the property") user.name = 'john smith' |
輸出為:
1 2 3 4 |
Traceback (most recent call last): File stdin, line 21, in mоdule user.name = 'john smith' AttributeError: can't set attribute |
使用屬性修飾符建立描述符
可以使用 Python 修飾符建立描述符,如 清單 4 所示。Python 修飾符是對 Python 語法的特定修改,能夠更方便地更改函式和方法。在本例中,將修改屬性管理方法。在 developerWorks 文章 Decorators make magic easy 中尋找更多有關應用 Python 修飾符的資訊。
清單 4. 使用屬性修飾符建立描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Person(object): def __init__(self): self._name = '' @property def name(self): print "Getting: %s" % self._name return self._name @name.setter def name(self, value): print "Setting: %s" % value self._name = value.title() @name.deleter def name(self): print ">Deleting: %s" % self._name del self._name |
在執行時建立描述符
前面的所有例子都使用了 name
屬性。該方法的侷限性在於需要對各個屬性分別覆蓋 __set__()
、__get__()
和 __delete__()
。清單 5 提供了一個可能的解決方案,幫助開發人員在執行時新增 property 屬性。該解決方案使用屬性型別構建資料描述符。
清單 5. 在執行時建立描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Person(object): def addProperty(self, attribute): # create local setter and getter with a particular attribute name getter = lambda self: self._getProperty(attribute) setter = lambda self, value: self._setProperty(attribute, value) # construct property attribute and add it to the class setattr(self.__class__, attribute, property(fget=getter, \ fset=setter, \ doc="Auto-generated method")) def _setProperty(self, attribute, value): print "Setting: %s = %s" %(attribute, value) setattr(self, '_' + attribute, value.title()) def _getProperty(self, attribute): print "Getting: %s" %attribute return getattr(self, '_' + attribute) |
讓我們執行這段程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> user = Person() >>> user.addProperty('name') >>> user.addProperty('phone') >>> user.name = 'john smith' Setting: name = john smith >>> user.phone = '12345' Setting: phone = 12345 >>> user.name Getting: name 'John Smith' >>> user.__dict__ {'_phone': '12345', '_name': 'John Smith'} |
這將在執行時建立 name
和 phone
屬性。它們可以根據相應的名稱進行訪問,但是按照 _setProperty 方法中的定義,將在物件名稱空間目錄中儲存為 _name 和 _phone。基本上,name
和 phone
是對內部的 _name 和 _phone 屬性的訪問符。
當開發人員嘗試新增 name
property 屬性時,您可能對系統中的 _name 屬性存在疑問。實際上,它將用新的 property 屬性覆蓋現有的 _name 屬性。這些程式碼允許控制如何在類內部處理屬性。
結束語
Python 描述符可以利用新的樣式類實現強大而靈活的屬性管理。通過結合使用描述符,可以實現優雅的程式設計,允許建立 Setters 和 Getters 以及只讀屬性。它還允許您根據值或型別請求進行屬性驗證。您可以在許多不同的領域應用描述符,但是使用時需保持謹慎的態度,避免由於覆蓋普通物件行為而產生不必要的程式碼複雜性。