iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0
Software Development

Python十翼:與未來的自己對話系列 第 10

[Day10] 四翼 - Descriptor:Non-Data Descriptor vs Data Descriptor

  • 分享至 

  • xImage
  •  

四翼大綱

一般我們從instance取得attributefunction時,會先由instance.__dict__找起,如果沒找到會再往上,順著生成instanceclassmro順序繼續找。descriptor是一種可以改變這種機制的有趣功能。雖然descriptor大部份應用會著重在由instance呼叫,這也會是我們接下來分享的重點,但是當其由class或是super呼叫時,各自有其細節要注意(註1)。

descriptor分為non-data descriptordata descriptornon-data descriptor為一有實作__get__class,而data descriptor為一有實作__get__加上__set__或是__delete__兩種其一的class

為方便稱呼,我將以desc_instance來稱呼由descriptor class生成的instance

  • [Day10]介紹non-data descriptordata descriptor
  • [Day11]分享可能具有潛在問題的descriptor實作方法。
  • [Day12]分享比較通用的descriptor實作方法。
  • [Day13]比較propertyDescriptor

non-data descriptor

當我們想由instance取得attributefunction時,如果遇到non-data descriptor,會先確認該attributefunction是否存在於instance.__dict__中,如果有的話會優先使用,如果沒有的話才會使用non-data descriptor__get__

__get__signature如下:

__get__(self, instance, owner_cls)
  • self是實作有__get__non-data descriptor class所生成的instance,我們稱作desc_instance
  • instancedesc_instance所在的class所生成的instance
  • owner_cls是生成instanceclass

乍看可能有點抽象,我們試著從# 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__

data descriptor

當我們由instance存取attributefunction時,如果遇到data descriptor,會使用其實作的__get____set__(相當於shadow instance.__dict__)。

__set__signature如下:

__set__(self, instance, value)
  • self是實作有__get____set__data descriptor class所生成的instance,即desc_instance本身。
  • instancedesc_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_desc10註2)。
  • 接下來我們使用my_inst.data_desc來取值,由於data_descshadow 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__

備註

註1:Descriptor HowTo Guide

註2:這邊我們必須使用這樣的語法,而不能使用my_inst.data_desc = 10,因為這樣會呼叫data_desc.__set__

參考資料

Descriptor相關內容,大部份整理自python-deepdive-Part 4-Section 08-DescriptorsDescriptor HowTo GuidePython Morsels練習題。

Code

本日程式碼傳送門


上一篇
[Day09] 三翼 - property:實例說明
下一篇
[Day11] 四翼 - Descriptor:Descriptor存取設計(1)
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言