iT邦幫忙

2023 iThome 鐵人賽

DAY 12
0
Software Development

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

[Day12] 四翼 - Descriptor:Descriptor存取設計(2)

  • 分享至 

  • xImage
  •  

今天我們來分享一些descriptor的通用寫法。由於今天的方法都能通過和方法1方法2類似的檢查,所以以下將不再特別說明。

方法5

方法5嘗試為每個desc_instance給定一個name,並將此name加上_作為instance variable存在instance中。

# 05
class Desc:
    def __init__(self, name):
        self._name = f'_{name}'

    def __get__(self, instance, owner_cls):
        if instance is None:
            return self
        return getattr(instance, self._name, None)

    def __set__(self, instance, value):
        setattr(instance, self._name, value)


class MyClass:
    x = Desc('x')
    y = Desc('y')

使用限制與缺點

  • 即使我們於程式中,已經知道desc_instancemy_inst中所用的名為xy,還是得再手動輸入一次'x''y'
  • 我們存於instance中的變數名,為underscore加上給定的名字。這是一個滿有討論空間的設定方法,大家都知道underscore暗示這是一個private attribute,但是我們不能保證這個名字沒有於MyClass中被使用。
  • 我們假設instance.__dict__是可用的。

方法6

方法6相當於Descriptor HowTo Guide中使用的方法,其和方法5類似,但是使用了__set_name__,這麼一來就不需要每次都手動指定名字了。MyClass中的'x''y'這兩個名字會自動傳遞給desc_instance。特別需要注意的是,如果在__set_name__中寫成self._name = name將會造成RecursionError。因為這麼一來self._name相當於'x''或'y'。當__get__藉由getattr(instance, self._name, None)來取值時,就像是呼叫my_inst.xmy_inst.y,可是xy都是data descriptor,其__get__會優先使用,這麼一來將會再次呼叫__get__

# 06
class Desc:
    def __get__(self, instance, owner_cls):
        if instance is None:
            return self
        return getattr(instance, self._name, None)

    def __set__(self, instance, value):
        setattr(instance, self._name, value)

    def __set_name__(self, owner_cls, name):
        self._name = f'_{name}'
        # self._name = name will raise RecursionError


class MyClass:
    x = Desc()
    y = Desc()       

使用限制與缺點

  • 方法6解決了方法5的第一個問題,但是_nameslots的問題還是存在。

方法7

方法7使用了instance.__dict__,並直接使用傳入的name(即self._name)作為instance.__dict__key,給定的value作為instance.__dict__value。乍看好像和方法6有些相似,但是這樣寫完全展現了對descriptor的高度了解。

  • 由於我們實作的是data descriptor,當由instance存取attribute時,會優先使用其__get____set__。所以我們可以直接使用其在MyClass中定義的名字,如'x''y',而不用使用類似'_x''_y'。這麼一來,我們也不用擔心'_x''_y'是否在MyClass的其它地方是否有被使用。
  • 請注意於__get____set__中,不能使用getattr(instance, self.name, None)setattr(instance, self.name, value),因為這樣會造成RecursionError,必須直接操作instance.__dict__
# 07
class Desc:
    def __get__(self, instance, owner_cls):
        if instance is None:
            return self
        return instance.__dict__.get(self._name)

    def __set__(self, instance, value):
        instance.__dict__[self._name] = value

    def __set_name__(self, owner_cls, name):
        self._name = name


class MyClass:
    x = Desc()
    y = Desc()

    
if __name__ == '__main__':
    my_inst = MyClass()
    my_inst.x = 0
    print(f'{my_inst.__dict__=}')  # {'x': 0}
    print(f'{my_inst.x=}')  # 0 

# 07中,當使用my_inst.x = 0時,會呼叫__set__,透過instance.__dict__[self._name] = value的語法將'x'作為key0value放入my_instinstance dict中(即my_inst.__dict__)。當我們使用my_inst.x時,會呼叫__get__,透過instance.__dict__.get(self._name)取回0

使用限制與缺點

  • 這是一個很優雅的實作方法,但是這個方法也是有slots的問題。

方法8

方法8方法3幾乎一樣,差別在於這邊的self._data是使用weakref.WeakKeyDictionary而不是dictWeakKeyDictionary顧名思義,儲存的是weak reference。所以此時,當我們使用del來刪除instance時,instance是可以被真正刪除的,且當self._data知道instance已被gc後,會自動刪除weak reference,這解決了我們頭痛的memoey leak問題。

from weakref import WeakKeyDictionary


# 08
class Desc:
    def __init__(self):
        self._data = WeakKeyDictionary()

    def __get__(self, instance, owner_cls):
        if instance is None:
            return self
        return self._data.get(instance)

    def __set__(self, instance, value):
        self._data[instance] = value


class MyClass:
    x = Desc()
    y = Desc()

使用限制與缺點

  • 方法8解決了方法3的第一個gc限制,但是仍然必須確定instancehashalbe,才能作為self._datakey
  • MyClass有使用slots時,記得要將__weakref__加入__slots__中。

方法9

方法9是一個比較複雜的實作,我們逐個function解說。

  • __init__中,建立一個dict作為倉庫,即# 09中的self._data
  • __set__中,需要決定self._datakeyvalue的型態。
    • key我們使用id(instance),這可以解決instance必須要是hashable的問題。
    • value我們使用一個tuple
      • 第一個元素為weakref.ref(instance, self._remove_object)weakref.ref可以幫忙建立對instanceweak reference,並在instancestrong reference0時,幫忙呼叫self._remove_object這個callback function
      • 第二個元素為真正想要由__get__回傳的值。
  • __get__中,開頭一樣先檢查instance是否為None,如果是None的話,代表是由class所呼叫,所以直接返回desc_instance。接下來我們使用walrus operator,來取self._data.get(id(instance))的回傳值。若能取到的話,代表其返回的是我們於__set__中所設定的tuple,從中取得第二個元素返回。若取不到的話,代表返回值為None,不做操作,並由Python隱性地返回None
  • _remove_object中,其接收weakref.ref所建的weak reference。我們針對self._data.items()打一個迴圈,從中比對是否有instance就是weak reference的情況,如果有就儲存key(id(instance))到found_key。接著檢查found_key,當其不為None時,代表self._data中有需要刪除的key-value pair,我們透過del self._data[found_key]來刪除。
import weakref


# 09
class Desc:
    def __init__(self):
        self._data = {}

    def __get__(self, instance, owner_cls):
        if instance is None:
            return self
        if value_tuple := self._data.get(id(instance)):
            return value_tuple[1]

    def __set__(self, instance, value):
        self._data[id(instance)] = (weakref.ref(
            instance, self._remove_object), value)

    def _remove_object(self, weak_ref):
        print(f'_remove_object called {weak_ref=}')
        found_key = None
        for key, (weak_ref_inst, _) in self._data.items():
            if weak_ref_inst is weak_ref:
                found_key = key
                break
        if found_key is not None:
            del self._data[found_key]


class MyClass:
    x = Desc()
    y = Desc()
    z = Desc()


if __name__ == '__main__':
    my_inst1 = MyClass()
    my_inst1.x = 1
    my_inst1.y = 2
    my_inst1.z = 3

    my_inst2 = MyClass()
    my_inst2.x = 11
    my_inst2.y = 22
    my_inst2.z = 33

    # {2462396503248: (<weakref at 0x0000023D5244A1B0; to 'MyClass' at 0x0000023D5244D4D0>, 1),
    #  2462396503504: (<weakref at 0x0000023D5244A250; to 'MyClass' at 0x0000023D5244D5D0>, 11)}
    print(MyClass.x._data)
    del my_inst1  # _remove_object called three times

如果觀察MyClass.x._data,可以看出其儲存了my_inst1my_inst2weak reference。當我們del my_inst1時,self._remove_object會被呼叫三遍,分別刪除my_inst1中的xyz三個desc_instance

使用限制與缺點

方法9幾乎可以適用於任何場景。但與方法8一樣,當MyClass有使用slots時,記得要將__weakref__加入__slots__中。

當日筆記

  • 方法5方法6非常像,只是方法6更為方便。
    • 因為__set_name__是Python3.6新加的功能,所以當要維護一些舊版本程式的時候,才比較有機會遇到方法5
    • 當做一些prototype或side project時,我們可以使用方法6,因為可以確定_name是沒有被使用且可以使用instance.__dict__,但還是需謹慎使用。
  • 方法7方法8是我們最常使用的方法:
    • instance.__dict__是可用的,我們會建議使用方法7
    • instance.__dict__是不可用的且instancehashable時,我們建議使用方法8
  • 方法9是一個考慮周詳的方法,不過除非真的遇到這麼刁鑽的情況,使用方法7方法8會來得更直觀簡單。
    • instance.__dict__是不可用的且instance不是hashable時,我們會建議使用方法9
  • class使用slots,且我們又使用如方法8方法9weakref方法時,記得將__weakref__加入__slots__中,否則將無法產生weakref

Code

本日程式碼傳送門


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

尚未有邦友留言

立即登入留言