語法簡析
一般來說,描述器(descriptor)是一個有”綁定行為”的對象屬性(object attribute)妓忍,它的屬性訪問被描述器協(xié)議方法重寫。這些方法是 __get__()
蔗怠、 __set__()
和 __delete__()
厢拭。如果一個對象定義了以上任意一個方法拴鸵,它就是一個描述器玷坠。而描述器協(xié)議的具體形式如下:
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
描述器本質(zhì)上是一個類對象,該對象定義了描述器協(xié)議三種方法中至少一種劲藐。而這三種方法只有當類的實例出現(xiàn)在一個所有者類(owner class)之內(nèi)時才有效八堡,也就是說,描述器必須出現(xiàn)在所有者類或其父類的字典 __dict__
里聘芜。這里提到了兩個類兄渺,一是定義了描述器協(xié)議的描述器類,另一個是使用描述器的所有者類厉膀。
描述器往往以裝飾器的方式被使用溶耘,導(dǎo)致二者常被混淆二拐。描述器類和不帶參數(shù)的裝飾器類一樣服鹅,都傳入函數(shù)對象作為參數(shù),并返回一個類實例百新,所不同的是企软,裝飾器類返回 callable 的實例,描述器則返回描述器實例饭望。
記住上面的話仗哨,下面我們舉例說明。
@Property
Python 內(nèi)置的 property
函數(shù)可以說是最著名的描述器之一铅辞,幾乎所有講述描述器的文章都會拿它做例子厌漂。
property
是用 C 實現(xiàn)的,不過這里有一份等價的 Python 實現(xiàn):
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__)
Property
怎么用呢斟珊?看下面的例子:
class C(object):
def __init__(self):
self._x = None
@Property
def x(self):
"""I'm the 'x' property."""
return self._x
@x.setter
def x(self, value):
assert value > 0
self._x = value
@x.deleter
def x(self):
del self._x
我們結(jié)合源代碼和用法來分析 Property
苇倡。
@Property
的用法就是一個裝飾器。我們可以將其等價轉(zhuǎn)化為:
x = Property(x)
函數(shù) x
作為位置參數(shù)被賦給 Property.__init__()
的 fget
囤踩,得到新的 x
已經(jīng)不是個函數(shù)而是個完整實現(xiàn)了 __get__()
方法的描述器實例了旨椒。
@x.setter
的用法略有不同。它實際上是利用上面定義的描述器實例 x
的 setter
方法堵漱,重新創(chuàng)建了新的實例综慎。這時變量 x
再次被更新,指向了一個完整實現(xiàn) __get__()
和 __set__()
方法的新描述器勤庐。傳入 setter
方法的函數(shù)名必須是 x
示惊,否則如果是 y
好港,按照裝飾器的性質(zhì),
y = x.setter(y)
新描述器就被 y
引用了米罚,與需求不符媚狰。
Property
提供了像訪問類“成員變量”一樣訪問 get、set 方法的能力阔拳。
In [123]: c = C()
In [124]: c.x = 1
In [125]: c.x
Out[125]: 1
In [126]: c.x = 0
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-126-b03deb420dcb> in <module>()
----> 1 c.x = 0
<ipython-input-50-95b8686aa4bd> in __set__(self, obj, value)
20 if self.fset is None:
21 raise AttributeError("can't set attribute")
---> 22 self.fset(obj, value)
23
24 def __delete__(self, obj):
<ipython-input-116-379a4e5fa639> in x(self, value)
10 @x.setter
11 def x(self, value):
---> 12 assert value > 0
13 self._x = value
14
AssertionError:
與一般的屬性訪問不同崭孤,c.x
訪問的已經(jīng)不是簡單的屬性,而是相當于 x.__get__(c)
糊肠,可以調(diào)用各種復(fù)雜方法對屬性作檢查辨宠、包裝 。
那么货裹,描述器是怎樣被訪問到的呢嗤形?
調(diào)用描述器
有兩類描述器:如果同時定義了 __get__()
和 __set__()
方法的描述器稱為資料描述器(data descriptor),僅定義了 __get__()
的描述器稱為非資料描述器(non-data descriptor)弧圆。非資料描述器常用于類的方法赋兵,如常見的 staticmethod
和 classmethod
,都是其應(yīng)用搔预。
如前文所說霹期,描述器常在所有者類或其實例中被調(diào)用。
對于實例對象拯田,object.__getattribute__()
會把 c.x
轉(zhuǎn)化為 type(c).__dict__['x'].__get__(c, type(c))
历造。如果實例中有和描述器重名的屬性 x
怎么辦?資料和非資料描述器的區(qū)別在于船庇,相對于實例字典的優(yōu)先級不同吭产。當描述器和實例字典中的某個屬性重名,按訪問優(yōu)先級鸭轮,資料描述器 > 同名實例字典中的屬性 > 非資料描述器臣淤,優(yōu)先級小的會被大的覆蓋。上面的類 C
中窃爷,會優(yōu)先訪問資料描述器 x
邑蒋。下面將講到,類的方法實際就是一個僅實現(xiàn)了 __get__()
的非資料描述器吞鸭,所以如果實例 c
中同時定義了名為 foo
的方法和屬性寺董,那么 c.foo
訪問的是屬性而非方法。
對于類刻剥,type.__getattribute__()
把 C.x
轉(zhuǎn)化為 C.__dict__['x'].__get__(None, C)
遮咖。
有幾點需要牢記的:
- 描述器被
__getattribute__()
方法調(diào)用 - 因而,重載
__getattribute__()
可能會妨礙描述器被自動調(diào)用 -
__getattribute__()
僅存在于繼承自object
的新式類之中 -
object.__getattribute__()
和type.__getattribute__()
對__get__()
的調(diào)用不一樣 - 資料描述器總會覆蓋實例字典造虏,即資料描述器具有最高優(yōu)先級
- 非資料描述器可能會被實例字典覆蓋御吞,即非資料描述器具有最低優(yōu)先級
非資料描述器與類方法
Python 面向?qū)ο蟮奶卣鹘⒃诨诤瘮?shù)的環(huán)境之上麦箍。Python 用非資料描述器將二者無縫結(jié)合。
方法和普通函數(shù)唯一的區(qū)別就是陶珠,一般方法的第一個參數(shù)引用了當前實例挟裂,即通常命名為 self
的變量。
Python 中的函數(shù)揍诽,可以被認為是一個實現(xiàn)了 __get__()
的非資料描述器诀蓉,用 Python 來描述就是:
class Function(object):
. . .
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
return types.MethodType(self, obj, objtype)
當函數(shù)作為屬性被訪問時,非資料描述器把函數(shù)變?yōu)橐粋€方法暑脆,把實例調(diào)用 obj.f(*args)
轉(zhuǎn)化成 f(obj, *args)
渠啤,把類調(diào)用 klass.f(*args)
轉(zhuǎn)化為 f(*args)
。
更多綁定和轉(zhuǎn)換參見下表添吗。
轉(zhuǎn)換 | 從對象調(diào)用 | 從類調(diào)用 |
---|---|---|
函數(shù) | f(obj, *args) | f(*args) |
靜態(tài)方法 | f(*args) | f(*args) |
類方法 | f(type(obj), *args) | f(klass, *args) |
靜態(tài)方法是特殊的方法沥曹,可以無須實例化而在類中被直接調(diào)用,這時當然無法提供合法的 self
碟联。為此妓美,需要實現(xiàn) staticmethod
描述器,其 __get__()
返回的函數(shù)無需實例參數(shù)鲤孵,其實也就是原樣返回即可壶栋,可以用 Python 這樣實現(xiàn)
class StaticMethod(object):
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
類方法是另一種特殊的方法,無需當前實例 self
裤纹, 但是需要當前類 klass
(通常也寫成 cls
)委刘,純 Python 實現(xiàn)如下:
class ClassMethod(object):
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc