當一個物件擁有__get__()方法(必要),以及選擇性的__set__()、__delete__()方法時,它可以作為描述器(Descriptor):
def __get__(self, instance, owner)
def __set__(self, instance, value)
def __delete__(self, instance)
def __set__(self, instance, value)
def __delete__(self, instance)
在Python中,所謂描述器,是用來描述特性的取得、設定、刪除該如何處理的物件,也就是說,當描述器實際是某個類別的特性成員時,對於類別特性的取得、設定或刪除,將會交由描述器來決定如何處理(除了那些內建特性,如__class__等特性之外)。例如:
class Descriptor:
def __get__(self, instance, owner):
print(self, instance, owner)
def __set__(self, instance, value):
print(self, instance, value)
def __delete__(self, instance):
print(self, instance)
class Some:
x = Descriptor()
在上例中,如果這麼執行:
s = Some()
s.x
s.x = 10
del s.x
s.x
s.x = 10
del s.x
其實相當於這麼作:
s = Some()
Some.__dict__['x'].__get__(s, Some);
Some.__dict__['x'].__set__(s, 10);
Some.__dict__['x'].__delete__(s);
Some.__dict__['x'].__get__(s, Some);
Some.__dict__['x'].__set__(s, 10);
Some.__dict__['x'].__delete__(s);
如果這麼作的話:
Some.x
則相當於這麼作:
Some.__dict__['x'].__get__(None, Some)
在 特性名稱空間 中談過特性搜尋的順序,依其中描述整理一下的話,特性的尋找順序是:
- 在實例的__dict__中尋找是否有相符的特性名稱
- 在產生實例的類別__dict__中尋找是否有相符的特性名稱
- 如果實例有定義__getattr__(),則看__getattr__()如何處理
- 如果實例沒有定義__getattr__(),則丟出AttributeError
如果加上描述器,則尋找的順序是:
- 在產生實例的類別__dict__中尋找是否有相符的特性名稱。如果找到 且實際是個描述器實例(也就是具有__get__()方法),且具有__set__()或__delete__()方法,若為取值,則傳回__get__ ()方法的值,若為設值,則呼叫__set__()(沒有這個方法則丟出AttributeError),若為刪除特性,則呼叫__delete__()(沒有這個方法則丟出AttributeError),如果描述器僅具有__get__(),則先進行第2步
- 在實例的__dict__中尋找是否有相符的特性名稱
- 在產生實例的類別__dict__中尋找是否有相符的特性名稱。如果不是描述器則直接傳回特性值。如果是個描述器(此時一定是僅具有__get__()方法),則傳回__get__()的值
- 如果實例有定義__getattr__(),則看__getattr__()如何處理
- 如果實例沒有定義__getattr__(),則丟出AttributeError
以上的流程可以作個簡單的驗證:
>>> class Desc:
... def __get__(self, instance, owner):
... print('instance', instance, 'owner', owner)
... def __set__(self, instance, value):
... print('instance', instance, 'value', value)
...
>>> class X:
... x = Desc()
...
>>> x = X()
>>> x.x
instance <__main__.X object at 0x01E01C10> owner <class '__main__.X'>
>>> x.x = 10
instance <__main__.X object at 0x01E01C10> value 10
>>> x.__dict__['x'] = 10
>>> x.x
instance <__main__.X object at 0x01E01C10> owner <class '__main__.X'>
>>> x.__dict__['x']
10
>>> del x.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
>>>
... def __get__(self, instance, owner):
... print('instance', instance, 'owner', owner)
... def __set__(self, instance, value):
... print('instance', instance, 'value', value)
...
>>> class X:
... x = Desc()
...
>>> x = X()
>>> x.x
instance <__main__.X object at 0x01E01C10> owner <class '__main__.X'>
>>> x.x = 10
instance <__main__.X object at 0x01E01C10> value 10
>>> x.__dict__['x'] = 10
>>> x.x
instance <__main__.X object at 0x01E01C10> owner <class '__main__.X'>
>>> x.__dict__['x']
10
>>> del x.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
>>>
除了__get__()方法之外,還具有__set__()或__delete__()方法或兩者兼具的描述器,稱之為資料描述器(Data descriptor),其行為與僅有__get__()方法的非資料描述器(Non-data descriptor)不同。例如以下為非資料描述器的行為:
>>> class Desc:
... def __get__(self, instance, owner):
... print('instance', instance, 'owner', owner)
...
>>> class X:
... x = Desc()
...
>>> x = X()
>>> x.x
instance <__main__.X object at 0x01E01FD0> owner <class '__main__.X'>
>>> x.x = 10
>>> x.x
10
>>> del x.x
>>> x.x
instance <__main__.X object at 0x01E01FD0> owner <class '__main__.X'>
>>>
... def __get__(self, instance, owner):
... print('instance', instance, 'owner', owner)
...
>>> class X:
... x = Desc()
...
>>> x = X()
>>> x.x
instance <__main__.X object at 0x01E01FD0> owner <class '__main__.X'>
>>> x.x = 10
>>> x.x
10
>>> del x.x
>>> x.x
instance <__main__.X object at 0x01E01FD0> owner <class '__main__.X'>
>>>
簡而言之,資料描述器可以讓你攔截對實例作特性的取得、設定與刪除行為,而非資料描述器可以讓你在攔截透過實例取得類別特性時的行為。
回顧 property() 函式 的內容,對於實例作特性的取得、設定與刪除,都會被轉呼叫為所指定的函式,可想而知的,這是一種資料描述器的行為,若要自行實作property()函式的行為,則可以如下:
def prop(getter, setter, deleter):
class PropDesc:
def __get__(self, instance, owner):
return getter(instance)
def __set__(self, instance, value):
setter(instance, value)
def __delete__(self, instance):
deleter(instance)
return PropDesc()
如此,property() 函式 中使用property()函式的例子,就可以改用以上的prop()函式,
class Ball:
def __init__(self, radius):
if radius <= 0:
raise ValueError('必須是正數')
self.__radius = radius
def getRadius(self):
return self.__radius
def setRadius(self, radius):
self.__radius = radius
def delRadius(self):
del self.__radius
radius = prop(getRadius, setRadius, delRadius)
在 靜態方法、類別方法 中討論過,類別的實例在操作類別所定義的方法時,方法的第一個參數都會被綁定為實例,透過實例所操作的這些方法稱之為綁定方法(Bound method)。例如:
>>> class Some:
... def doSome(self):
... print('something...', self)
...
>>> s = Some()
>>> s.doSome()
something... <__main__.Some object at 0x01DA1C50>
>>> s.doSome
<bound method Some.doSome of <__main__.Some object at 0x01DA1C50>>
>>> Some.doSome('arguments')
something... arguments
>>> Some.doSome
<function doSome at 0x01D303D8>
>>>
... def doSome(self):
... print('something...', self)
...
>>> s = Some()
>>> s.doSome()
something... <__main__.Some object at 0x01DA1C50>
>>> s.doSome
<bound method Some.doSome of <__main__.Some object at 0x01DA1C50>>
>>> Some.doSome('arguments')
something... arguments
>>> Some.doSome
<function doSome at 0x01D303D8>
>>>
很顯然地,透過實例所操作的方法,與原先定義在Some類別上的函式是不同的。事實上,你可以這麼操作:
>>> Some.__dict__['doSome'].__get__(s, Some)()
something... <__main__.Some object at 0x01DA1C50>
>>> Some.__dict__['doSome'].__get__(s, Some)
<bound method Some.doSome of <__main__.Some object at 0x01DA1C50>>
>>> Some.__dict__['doSome'].__get__(None, Some)()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: doSome() takes exactly 1 positional argument (0 given)
>>> Some.__dict__['doSome'].__get__(None, Some)('arguments')
something... arguments
>>> Some.__dict__['doSome'].__get__(None, Some)
<function doSome at 0x01D303D8>
>>>
something... <__main__.Some object at 0x01DA1C50>
>>> Some.__dict__['doSome'].__get__(s, Some)
<bound method Some.doSome of <__main__.Some object at 0x01DA1C50>>
>>> Some.__dict__['doSome'].__get__(None, Some)()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: doSome() takes exactly 1 positional argument (0 given)
>>> Some.__dict__['doSome'].__get__(None, Some)('arguments')
something... arguments
>>> Some.__dict__['doSome'].__get__(None, Some)
<function doSome at 0x01D303D8>
>>>
顯然地,Some類別上的doSome特性所參考的物件,具有__get__()方法,也就是說doSome特性實際上是個描述器,在Python類別中定義的函式,實際上是個特性名稱參考至一個非資料描述器。
假設你有個類別如下:
class Some:
def doSome(self, arg):
print(self, arg)
s = Some()
s.doSome(10)
Some.doSome(10, 20)
可以嘗試自行使用描述器來「模擬」上面的Some類別doSome的行為,以大致可以了解Python中對於綁定方法的原理:
class DoSomeDesc:
def doSome(self, arg):
print(self, arg)
def __get__(self, instance, owner):
if instance:
return lambda arg: DoSomeDesc.doSome(instance, arg)
else:
return DoSomeDesc.doSome
class Some:
doSome = DoSomeDesc()
s = Some()
s.doSome(10)
Some.doSome(10, 20)