描述符(Descriptors)是Python語言中一個深奧但卻重要的一部分。它們廣泛應用於Python語言的核心,熟練掌握描述符將會為Python程式設計師的工具箱新增一個額外的技巧。為了給接下來對描述符的討論做一些鋪墊,我將描述一些程式設計師可能會在日常程式設計活動中遇到的場景,然後我將解釋描述符是什麼,以及它們如何為這些場景提供優雅的解決方案。在這篇總結中,我會使用新樣式類來指代Python版本。
1、假設一個程式中,我們需要對一個物件屬性執行嚴格的型別檢查。然而,Python是一種動態語言,所以並不支援型別檢查,但是這並不妨礙我們實現自己版本,且較為初級的型別檢查。物件屬性型別檢查的傳統方法可能採用下面的方式:
1 2 3 4 5 6 7 8 9 |
def __init__(self, name, age): if isinstance(str, name): self.name = name else: raise TypeError("Must be a string") if isinstance(int, age): self.age = age else: raise TypeError("Must be an int") |
上面是執行這種型別檢查的一種方法,但是引數數量增加時它將變得比較繁瑣。另外,在賦值之前,我們可以建立一個在__init__中呼叫的type_check(type, val)函式,但是當我們想在其他地方設定屬性值時,該如何簡單地實現這種檢查呢。我想到的一個快速解決方案是Java中的getters和setters,但是這並不符合Python風格,並且比較麻煩。
2、假設在一個程式中,我們想建立一些在執行時立刻初始化然後變成只讀的屬性。有人也能想到利用Python中的特殊方法來實現,但這種實現方法仍舊是笨拙和繁瑣的。
3、最後,設想一個程式中,我們希望以某種方式自定義物件屬性的訪問。例如需要記錄這種屬性的訪問。同樣的,還是可以想到一個解決方法,即使這種解決方案可能比較笨重並且不可複用。
上述問題因都與屬性引用相關而全部聯絡在了一起。下面,我們將嘗試自定義屬性的訪問方法。
Python描述符
針對上面所列的問題,描述符提供了優雅、簡潔、健壯和可重用的解決方案。簡而言之,一個描述符就是一個物件,該物件代表了一個屬性的值。這就意味著如果一個賬戶物件有一個屬性“name”,那麼描述符就是另一個能夠用來代表屬性“name”持有值的物件。描述符協議中“定義了__get__”、“__set__”或”__delete__” 這些特殊方法,描述符是實現其中一個或多個方法的物件。這些方法中每一種方法的簽名如下所示:python descr.get(self,obj,type=None)->value。
1 2 3 |
descr.__set__(self, obj, value) --> None descr.__delete__(self, obj) --> None |
實現__get__方法的物件是非資料描述符,意味著在初始化之後它們只能被讀取。而同時實現__get__和__set__的物件是資料描述符,意味著這種屬性是可寫的。
為了更好地理解描述符,我們給出針對上述問題基於描述符的解決方法。使用Python描述符實現物件屬性的型別檢查將是一個非常簡單的任務。裝飾器實現這種型別檢查的程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class TypedProperty(object): def __init__(self, name, type, default=None): self.name = "_" + name self.type = type self.default = default if default else type() def __get__(self, instance, cls): return getattr(instance, self.name, self.default) def __set__(self,instance,value): if not isinstance(value,self.type): raise TypeError("Must be a %s" % self.type) setattr(instance,self.name,value) def __delete__(self,instance): raise AttributeError("Can't delete attribute") class Foo(object): name = TypedProperty("name",str) num = TypedProperty("num",int,42) >> acct = Foo() >> acct.name = "obi" >> acct.num = 1234 >> print acct.num 1234 >> print acct.name obi # trying to assign a string to number fails >> acct.num = '1234' TypeError: Must be a <type 'int'> |
在這個例子中,我們實現了一個描述符TypedProperty,並且這個描述符類會對它所代表的類的任何屬性執行型別檢查。注意到這一點很重要,即描述符只能在類級別進行合法定義,而不能在例項級別定義。例如,在上面例子中的__init__方法裡。
當訪問類Foo例項的任何屬性時,描述符會呼叫它的__get__方法。需要注意的是,__get__方法的第一個引數是描述符代表的屬性被引用的源物件。當屬性被分配時,描述符會呼叫它的__set__方法。為了理解為什麼可以使用描述符代表物件屬性,我們需要理解Python中屬性引用解析的執行方式。對於物件來說,屬性解析機制在object.__getattribute__()中。該方法將b.x轉換成type(b).__dict__[‘x’].__get__(b, type(b))。然後,解析機制使用優先順序鏈搜尋屬性,在優先順序鏈中,類字典中發現的資料描述符的優先順序高於例項變數,例項變數優先順序高於非資料描述符,如果提供了getattr(),優先順序鏈會為getattr()分配最低優先順序。對於一個給定的物件類,可以通過自定義__getattribute__方法來重寫優先順序鏈。
深刻理解優先順序鏈之後,就很容易想出針對前面提出的第二個和第三個問題的優雅解決方案了。那就是,利用描述符實現一個只讀屬性將變成實現資料描述符這個簡單的情況了,即不帶__set__方法的描述符。儘管在本例中不重要,定義訪問方式的問題只需要在__get__和__set__方法中增加所需的功能即可。
類屬性
每次我們想使用描述符的時候都不得不定義描述符類,這樣看起來非常繁瑣。Python特性提供了一種簡潔的方式用來向屬性增加資料描述符。一個屬性簽名如下所示:
1 |
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute |
fget、fset和fdel分別是類的getter、setter和deleter方法。我們通過下面的一個示例來說明如何建立屬性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Accout(object): def __init__(self): self._acct_num = None def get_acct_num(self): return self._acct_num def set_acct_num(self, value): self._acct_num = value def del_acct_num(self): del self._acct_num acct_num = property(get_acct_num, set_acct_num, del_acct_num, "Account number property.") |
如果acct是Account的一個例項,acct.acct_num將會呼叫getter,acct.acct_num = value將呼叫setter,del acct_num.acct_num將呼叫deleter。
在Python中,屬性物件和功能可以像《描述符指南》中說明的那樣使用描述符協議來實現,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Property(object): "Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError("unreadable attribute") return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError("can't set attribute") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError("can't delete attribute") self.fdel(obj) def getter(self, fget): return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset): return type(self)(self.fget, fset, self.fdel, self.__doc__) def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__) |
Python也提供了@ property裝飾器,可以用它來建立只讀屬性。一個屬性物件擁有getter、setter和deleter裝飾器方法,可以使用它們通過對應的被裝飾函式的accessor函式建立屬性的拷貝。下面的例子最好地解釋了這一點:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class C(object): def __init__(self): self._x = None @property # the x property. the decorator creates a read-only property def x(self): return self._x @x.setter # the x property setter makes the property writeable def x(self, value): self._x = value @x.deleter def x(self): del self._x |
如果我們想讓屬性只讀,那麼我們可以去掉setter方法。
在Python語言中,描述符有著廣泛的應用。Python函式、類方法、靜態方法都是非資料描述符的例子。針對列舉的Python物件是如何使用描述符實現的問題,《描述符指南》給出了一個基本的描述。
擴充套件閱讀
1. 描述符指南。
2. Python參考手冊,第四版,David Beazley
4. 深入新樣式類