今天我們來分享一些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
。