一般我們從instance取得attribute或function時,會先由instance.__dict__找起,如果沒找到會再往上,順著生成instance的class的mro順序繼續找。descriptor是一種可以改變這種機制的有趣功能。雖然descriptor大部份應用會著重在由instance呼叫,這也會是我們接下來分享的重點,但是當其由class或是super呼叫時,各自有其細節要注意(註1)。
descriptor分為non-data descriptor及data descriptor。non-data descriptor為一有實作__get__的class,而data descriptor為一有實作__get__加上__set__或是__delete__兩種其一的class。
為方便稱呼,我將以desc_instance來稱呼由descriptor class生成的instance。
non-data descriptor與data descriptor。descriptor實作方法。descriptor實作方法。property 與Descriptor。當我們想由instance取得attribute或function時,如果遇到non-data descriptor,會先確認該attribute或function是否存在於instance.__dict__中,如果有的話會優先使用,如果沒有的話才會使用non-data descriptor的__get__。
__get__的signature如下:
__get__(self, instance, owner_cls)
self是實作有__get__的non-data descriptor class所生成的instance,我們稱作desc_instance。instance是desc_instance所在的class所生成的instance。owner_cls是生成instance的class。乍看可能有點抽象,我們試著從# 01的例子中來說明。
其中:
self就是MyClass中的non_data_desc。instance就是my_inst。owner_cls就是MyClass。# 01
class NonDataDescriptor:
def __get__(self, instance, owner_cls):
print('NonDataDescriptor __get__ called')
class MyClass:
non_data_desc = NonDataDescriptor()
if __name__ == '__main__':
my_inst = MyClass()
print(f'{my_inst.__dict__=}') # {}
print(f'{my_inst.non_data_desc=}') # None
my_inst.non_data_desc = 10 # shadow
print(f'{my_inst.non_data_desc=}') # 10
print(f'{my_inst.__dict__=}') # {'non_data_desc': 10}
print(f'{my_inst.non_data_desc=}') # 10
my_inst.__dict__={}
NonDataDescriptor __get__ called
my_inst.non_data_desc=None
my_inst.non_data_desc=10
my_inst.__dict__={'non_data_desc': 10}
my_inst.non_data_desc=10
my_inst剛由MyClass生成時,my_inst.__dict__為一個空的dict。my_inst.non_data_desc來取值,由於my_inst.__dict__找不到non_data_desc,所以會繼續使用non_data_desc.__get__來取值。而我們在__get__中只有印出參數,所以回傳值為None。my_inst.non_data_desc=10來賦值,這相當於在my_inst.__dict__中添加non_data_desc為10。這可以由再次觀察my_inst.__dict__來驗證。my_inst.non_data_desc來取值,因為my_inst.__dict__中已經有non_data_desc,所以會回傳10,而不會呼叫non_data_desc.__get__。當我們由instance存取attribute或function時,如果遇到data descriptor,會使用其實作的__get__及__set__(相當於shadow instance.__dict__)。
__set__的signature如下:
__set__(self, instance, value)
self是實作有__get__及__set__的data descriptor class所生成的instance,即desc_instance本身。instance是desc_instance所在的class所生成的instance。value是所傳入想指定的值。從# 02的例子中來說明。
其中:
self就是MyClass中的data_desc。instance就是my_inst。value就是20。# 02
class DataDescriptor:
def __get__(self, instance, owner_cls):
print('DataDescriptor __get__ called')
def __set__(self, instance, value):
print(f'DataDescriptor __set__ called, {value=}')
class MyClass:
data_desc = DataDescriptor()
if __name__ == '__main__':
my_inst = MyClass()
print(f'{my_inst.__dict__=}') # {}
my_inst.__dict__['data_desc'] = 10
print(f'{my_inst.data_desc=}') # None
my_inst.data_desc = 20 # always use data_desc.__set__
print(f'{my_inst.data_desc=}') # None
print(f'{my_inst.__dict__=}') # {}
my_inst.__dict__={}
DataDescriptor __get__ called
my_inst.data_desc=None
DataDescriptor __set__ called, value=20
DataDescriptor __get__ called
my_inst.data_desc=None
my_inst.__dict__={'data_desc': 10}
my_inst剛由MyClass生成時,my_inst.__dict__為一個空的dict。my_inst.__dict__中手動插入data_desc為10(註2)。my_inst.data_desc來取值,由於data_desc會shadow instance.__dict__,所以將會呼叫data_desc.__get__。而我們在__get__中只有印出參數,所以回傳值為None。my_inst.data_desc=20來賦值,這會呼叫data_desc.__set__來進行賦值(但__set__目前僅呼叫一次print,並未實際賦值)。my_inst.data_desc來取值,此語法仍會呼叫data_desc.__get__,並回傳None。此時如果再次驗證my_inst.__dict__,會發現其中只有我們剛剛手動插入的data_desc,其值依然為10。non-data descriptor class生成的desc_instance可能會被instance.__dict__ shadow 。data descriptor class生成的desc_instance必定會shadow instacne.__dict__ 。註2:這邊我們必須使用這樣的語法,而不能使用my_inst.data_desc = 10,因為這樣會呼叫data_desc.__set__。
Descriptor相關內容,大部份整理自python-deepdive-Part 4-Section 08-Descriptors、Descriptor HowTo Guide及Python Morsels練習題。