今天我們來分享一些descriptor的通用寫法。由於今天的方法都能通過和方法1及方法2類似的檢查,所以以下將不再特別說明。
方法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_instance於my_inst中所用的名為x或y,還是得再手動輸入一次'x'或'y'。instance中的變數名,為underscore加上給定的名字。這是一個滿有討論空間的設定方法,大家都知道underscore暗示這是一個private attribute,但是我們不能保證這個名字沒有於MyClass中被使用。instance.__dict__是可用的。方法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.x或my_inst.y,可是x及y都是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的第一個問題,但是_name及slots的問題還是存在。方法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'作為key,0為value放入my_inst的instance dict中(即my_inst.__dict__)。當我們使用my_inst.x時,會呼叫__get__,透過instance.__dict__.get(self._name)取回0。
slots的問題。方法8與方法3幾乎一樣,差別在於這邊的self._data是使用weakref.WeakKeyDictionary而不是dict。WeakKeyDictionary顧名思義,儲存的是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限制,但是仍然必須確定instance是hashalbe,才能作為self._data的key。MyClass有使用slots時,記得要將__weakref__加入__slots__中。方法9是一個比較複雜的實作,我們逐個function解說。
__init__中,建立一個dict作為倉庫,即# 09中的self._data。__set__中,需要決定self._data中key與value的型態。
key我們使用id(instance),這可以解決instance必須要是hashable的問題。value我們使用一個tuple。
weakref.ref(instance, self._remove_object),weakref.ref可以幫忙建立對instance的weak reference,並在instance的strong reference為0時,幫忙呼叫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_inst1及my_inst2的weak reference。當我們del my_inst1時,self._remove_object會被呼叫三遍,分別刪除my_inst1中的x、y及z三個desc_instance。
方法9幾乎可以適用於任何場景。但與方法8一樣,當MyClass有使用slots時,記得要將__weakref__加入__slots__中。
方法5和方法6非常像,只是方法6更為方便。
__set_name__是Python3.6新加的功能,所以當要維護一些舊版本程式的時候,才比較有機會遇到方法5。方法6,因為可以確定_name是沒有被使用且可以使用instance.__dict__,但還是需謹慎使用。方法7和方法8是我們最常使用的方法:
instance.__dict__是可用的,我們會建議使用方法7。instance.__dict__是不可用的且instance是hashable時,我們建議使用方法8。方法9是一個考慮周詳的方法,不過除非真的遇到這麼刁鑽的情況,使用方法7或方法8會來得更直觀簡單。
instance.__dict__是不可用的且instance不是hashable時,我們會建議使用方法9。class使用slots,且我們又使用如方法8或方法9的weakref方法時,記得將__weakref__加入__slots__中,否則將無法產生weakref。