descriptor就像是Python的倉庫管理員之一,在某些情況下(如[Day10]所述):
non-data descriptor時,可以提供取的功能。data descriptor時,同時提供存取功能。所以要從哪邊取值及要將值存去哪裡,是寫descriptor時,最須注意的地方。
一般來說,有兩個地方可以考慮,一個是desc_instance內,另一個是instance.__dict__,兩者都各有一些眉角。
接下來兩天,我們將練習數個data descriptor的寫法(註1)。今天會先分享一些有潛在問題的寫法,透過了解各方法的缺點或限制,漸漸學習反思。明天再分享一些通用的寫法。
以下將會用desc_instance來代稱data descriptor instance。
方法1試著將給定的值作為instance variable存在desc_instance內,即# 01中的self._value。
# 01
class Desc:
def __get__(self, instance, owner_cls):
return self._value
def __set__(self, instance, value):
self._value = value
class MyClass:
x = Desc()
if __name__ == '__main__':
my_inst1, my_inst2 = MyClass(), MyClass()
# my_inst1
my_inst1.x = 1
print(f'{my_inst1.x=}') # 1
# my_inst2
print(f'{my_inst2.x=}') # 1
my_inst2.x = 2
print(f'{my_inst2.x=}') # 2
# my_inst1.x also changed
print(f'{my_inst1.x=}') # 2...not 1
my_inst1.x=1
my_inst2.x=1
my_inst2.x=2
my_inst1.x=2
MyClass中建立名為x的desc_instance。my_inst1及my_inst2兩個instance。此時若使用my_inst1.x或my_inst2.x,將會呼叫x.__get__取值:若使用my_inst1.x = 1或my_inst2.x = 2,將會呼叫x.__set__賦值。my_inst1.x = 1將1指定給x內的self._value,並確認my_inst1.x的確為1。my_inst2.x,發現其已經有1這個值。這是因為x現在是一個class variable,所以my_inst2.x就是my_inst1.x。my_inst2.x = 2會將2指定給x內的self._value,也就是說my_inst1.x及my_inst2.x的回傳值都會是2。my_inst1.x = 1的語法,指定值給self._value。MyClass生成的instance都擁有能修改x的權力。方法2試著將給定的值作為instance variable存在instance本身,即# 02中的instance.hardcoded_name。
# 02
class Desc:
def __get__(self, instance, owner_cls):
return getattr(instance, 'hardcoded_name', None)
def __set__(self, instance, value):
setattr(instance, 'hardcoded_name', value)
class MyClass:
x = Desc()
y = Desc()
if __name__ == '__main__':
my_inst = MyClass()
print(f'{my_inst.__dict__=}') # {}
my_inst.x = 1
print(f'{my_inst.x=}') # 1
print(f'{my_inst.__dict__=}') # {'hardcoded_name': 1}
my_inst.x = 2
print(f'{my_inst.x=}') # 2
print(f'{my_inst.y=}') # 2
print(f'{my_inst.__dict__=}') # {'hardcoded_name': 2}
my_inst.__dict__={}
my_inst.x=1
my_inst.__dict__={'hardcoded_name': 1}
my_inst.x=2
my_inst.y=2
my_inst.__dict__={'hardcoded_name': 2}
MyClass中建立x及y兩個desc_instance。my_inst.__dict__為一空dict。my_inst.x = 1將1指定給my_inst.hardcoded_name,透過再次觀察my_inst.__dict__,可以確認hardcoded_name已在instance.__dict__中,且其值為1。my_inst只有準備一個hardcoded_name來作為存取descriptor的倉庫。所以當我們使用my_inst.x = 2時,其實相當於將2指定給my_inst.hardcoded_name,透過再次觀察MyClass.__dict__,可以確認hardcoded_name已在instance.__dict__中,且其值已變為2。MyClass中,所有Desc生成的desc_instance將會共享一個固定的instance variable,即instance.hardcoded_name。方法3嘗試於descriptor內建立一個dict,即# 03的self._data。我們以各instance本身為self._data的key,於__set__給定的value為self._data的value。
# 03
class Desc:
def __init__(self):
self._data = {}
def __get__(self, instance, owner_cls):
return self._data.get(instance)
def __set__(self, instance, value):
self._data[instance] = value
class MyClass:
x = Desc()
y = Desc()
如果進行和方法1及方法2類似的檢查,可以發現方法3也沒有類似的問題,但卻有一個非常大的缺點。
self._data是將instance本身作為key,這代表即使我們手動使用del指令刪除了instance,也會是個假象,instance不會被gc(garbage collect),因為至少還有一個strong reference存在。這是個嚴重的memoey leak,該被gc的obj卻還是存在,且有機會被存取。memoey leak,我們還必須確定instance是hashalbe,才能作為self._data的key。方法4類似於方法3,但這次我們使用id(instance)為self._data的key。
# 04
class Desc:
def __init__(self):
self._data = {}
def __get__(self, instance, owner_cls):
return self._data.get(id(instance))
def __set__(self, instance, value):
self._data[id(instance)] = value
class MyClass:
x = Desc()
y = Desc()
如果進行和方法1及方法2類似的檢查,可以發現方法4也沒有類似的問題,且如果我們利用del來刪除instance時,該instance也真的會被gc。僅管如此,方法4還是有一些缺點。
instance,其記憶體位置id(instance),仍然以int型態存在self._data中。乍聽好像沒什麼關係,但是Python的記憶體位置是會重複使用的。如果這個記憶體位置被其它obj使用了,我們的descriptor就隱含了這個不相關obj的資訊(雖然機率極低)。descriptor,也會造成很多無謂的記憶體浪費。上面幾個方法讓我們對descriptor有了基本的認知。現在我們來充充電,講幾個實作descriptor時常會用到的觀念,為明天descriptor的通用寫法做好準備。
desc_instance當我們應用descriptor時,有時需要取得desc_instance,一個常見的做法如下:
def __get__(self, instance, owner_cls):
if instance is None:
return self
...
在__get__一開始,先判斷instance是不是None。如果是None,代表我們是由class來取,直接返回desc_instance。也就是說,當我們想取得desc_instance時,可以利用MyClass.desc來取得。雖然我們大多數情況都是使用instance呼叫desc_instance,但當想觀察desc_instance內部狀態時,可以透過這個小技巧來達成。從明天的方法5開始,我們會於__get__中加上這一段程式碼。
__set_name____set_name__的signature如下:
__set_name__(self, owner_cls, name)
class在定義時,會自動尋找有實作__set_name__的attribute,透過這個方法將他們在class的名字傳入desc_instance(註2)。舉例來說,如果Desc實作有__set_name__,那麼MyClass中的'x'(str型態)於MyClass定義時,就會自動透過x.__set_name__傳入desc_instance。
class MyClass:
x = Desc()
這個功能非常方便,可以讓我們在設計descriptor時,取得於MyClass中定義的名字。
__slots__當class實作有__slots__(一般定義為tuple),未被列在其中的attribute,將無法由instance存取,這包括我們一直習以為常使用的__dict__。當然您也可以選擇使用__slots__然後手動將__dict__加進__slots__。
由# 101可以看出,當MyClass的__slots__設定為空的tuple時,我們無法使用my_inst.__dict__。但是在MyClass2中,我們手動加入__dict__後,就可以存取my_inst2.__dict__了。
所以當我們說一個instance.__dict__可用時,代表其class未使用slots又或者有使用slots但是有將__dict__加進__slots__。
# 101
class MyClass:
__slots__ = ()
class MyClass2:
__slots__ = ('__dict__',)
if __name__ == '__main__':
my_inst = MyClass()
print(f'{my_inst.__dict__=}') # AttributeError
my_inst2 = MyClass2()
print(f'{my_inst2.__dict__=}') # {}
或許您會想,應該不會有很多Class使用slots吧?但事實上,當程式需要大量由同個class生成的instance時,選擇使用slots,可以節省滿多的記憶體消耗。在一些與database相關的ORM應用或iterator class的實作上不算少見。
weak referencePython內建有weakref module,可以讓我們建立對某obj的weak reference。當該obj的strong reference為0時,此weak reference會收到通知,並且在有提供callback function時,呼叫這個function。而weakref.WeakKeyDictionary是一個可以自動幫我們建立及移除weak reference的方便容器。
想要能夠建立weak reference,其必須有__weakref__ attribute。__weakref__是一個data descriptor,當我們對一個obj建立weak reference時,這個weak reference object其實就是存在__weakref__中。
當使用slots時,必須手動將__weakref__加進__slots__,否則將無法建立weak reference。
class MyClass:
__slots__ = ('__weakref__',)
註1:至於想實作non-data descriptor的朋友,相信可以由比較複雜的data descriptor中融會貫通而來。
註2:可以參考Data Model對這部份的說明。