會想要研究這個小project,是因為在實作project ECC時,想要動態傳遞ttl。原先以為不太難,很快可以搞定,可是在玩了一陣子之後發現,當中有不少有趣的地方,所以想要有系統地做成筆記。
postman是一個decorator factory,其接受一個item作為參數。wrapper內有一個print來幫忙確認傳進來的item值。
# postman.py
from functools import wraps
def postman(item):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
print(f'{item=} is received')
return fn(*args, **kwargs)
return wrapper
return decorator
我們的目標是研究,有幾種方法可以動態傳遞item給postman,並裝飾(更精準的說法應該是apply)於# 00中Class的func上。
# 00
from postman import postman
class Class:
def func(self):
...
理論上,Class的func應該是non-data descriptor instance,但是為方便描述,我們將稱呼其為non-data descriptor。
instance.__dict__func為non-data descriptor,其由instance存取時,會返回由types.MethodType生成的bound method,所以__init__中一開始的print(type(self.func))為method。postman(item)會返回帶有item資訊的decorator function,所以postman(item)(self.func)相當於decorator(self.func),會返回一個帶有item資訊的wrapper function。func為non-data descriptor,其沒有__set__,所以self.func = postman(item)(self.func)相當於將帶有item資訊的wrapper function存於inst.__dict__中。此時觀察print(type(self.func))可以發現其已經變為function。inst.func時,相當於呼叫wrapper。其會呼叫self.func這個bound method後回傳。由於self.func是bound method,self會自己傳遞,所以inst.func使用起來就像是一般的instance method。func由inst.__dict__中移除,我們還是可以使用inst.func()的語法來呼叫原來的func。# 01
from postman import postman
class Class:
def __init__(self, item):
print(type(self.func)) # method
self.func = postman(item)(self.func)
print(type(self.func)) # function
def func(self):
...
if __name__ == '__main__':
inst = Class('xmas_card')
inst.func() # item='xmas_card' is received
print(vars(inst)) # {'func': <function Class.func at 0x0000023BF70C6840>}
del vars(inst)['func']
inst.func() # nothing shown on the screen
雖然方法1只有短短幾行,但中間發生了很多事情。我們認為這是個hacky的方法,production code可能不適合這麼寫。
__new__方法2與方法1類似,只是我們改在__new__來做。
# 02
from postman import postman
class Class:
def __new__(cls, item):
inst = super().__new__(cls)
inst.func = postman(item)(inst.func)
return inst
def func(self):
...
if __name__ == '__main__':
inst = Class('xmas_card')
inst.func() # item='xmas_card' is received
__new__方法3於生成instance前,先使用了cls.func = postman(item)(cls.func)後,再生成instance。
這麼做有個大問題,而且如果只利用Class生成一個instance的話,或許還觀察不出來。由於__new__是「每次」Class需要生成instance的時候,都會被呼叫一次,所以相當於每次生成instance,都會做一次cls.func = postman(item)(cls.func)。
# 03中我們只呼叫了一次inst.func,卻看到三個訊息被印出,因為 Class('xmas_card')、 Class('mail')及Class('package')分別於__new__中都mutate了一次func。
# 03
from postman import postman
class Class:
def __new__(cls, item):
cls.func = postman(item)(cls.func)
inst = super().__new__(cls)
return inst
def func(self):
...
if __name__ == '__main__':
Class('xmas_card'), Class('mail')
inst = Class('package')
inst.func()
# item='package' is received
# item='mail' is received
# item='xmas_card' is received
方法4我們寫了一個名為dec的decorator,來裝飾在Class上。
dec接受一個item參數後,返回一個wrapper function。wrapper接收一個cls參數,我們使用vars(cls).get(name)看看能不能從cls.__dict__中取到func。如果有取到的話,再進一步判斷其是否為callable。如果是的話,使用setattr重新設定cls.name為postman(item)(obj))。由於我們mutate了cls,所以不管是在mutate前或後生成的instance都將會受影響。mutate的語法可以直接使用dec('xmas_card')(Class),不過如果想寫成Class = dec('xmas_card')(Class)或是於Class上加上@dec('xmas_card')也是可以的。
# 04
from postman import postman
def dec(item):
def wrapper(cls):
name = 'func'
if obj := vars(cls).get(name):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
return wrapper
class Class:
def func(self):
...
if __name__ == '__main__':
inst = Class()
dec('xmas_card')(Class)
inst2 = Class()
inst2.func() # item='xmas_card' is received
inst.func() # item='xmas_card' is received
由於我們是針對vars(cls)來進行搜尋,所以如# 04a中func定義於ParentClass而非Class的情況,此方法並不適用。
# 04a
...
class ParentClass:
def func(self):
...
class Class(ParentClass):
pass
if __name__ == '__main__':
inst = Class()
dec('xmas_card')(Class)
inst.func() # nothing shown on the screen
方法5與方法4類似,但我們使用getattr來尋找cls.name。
# 05
from postman import postman
def dec(item):
def wrapper(cls):
name = 'func'
if obj := getattr(cls, name, None):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
return wrapper
class Class:
def func(self):
...
if __name__ == '__main__':
inst = Class()
dec('xmas_card')(Class) # Class is mutated
inst.func() # item='xmas_card' is received
由於這是class級別的lookup(可以參考[Day18]),所以如# 05a中func定義於ParentClass而非Class的情況,此方法也可適用。
# 05a
...
class ParentClass:
def func(self):
...
class Class(ParentClass):
pass
if __name__ == '__main__':
inst = Class()
dec('xmas_card')(Class)
inst.func() # item='xmas_card' is received
方法6使用了與方法4類似的邏輯,只是這次使用了metaclass而不是decorator。此時item需要於Class生成時,以keyword-only argument傳遞,並需要記得於Meta.__new__中加入item的signature,才能存取到item。
# 06
from postman import postman
class Meta(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, item):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
name = 'func'
if obj := vars(cls).get(name):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
item = 'xmas_card'
class Class(metaclass=Meta, item=item):
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
與# 04a一樣,# 06a這種繼承的情況也不適用。
# 06a
...
item = 'xmas_card'
class ParentClass:
def func(self):
...
class Class(ParentClass, metaclass=Meta, item=item):
...
if __name__ == '__main__':
inst = Class()
inst.func() # nothing shown on the screen
方法7與方法6類似,但我們使用getattr來尋找cls.name。
# 07
from postman import postman
class Meta(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, item):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
name = 'func'
if obj := getattr(cls, name, None):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
item = 'xmas_card'
class Class(metaclass=Meta, item=item):
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
與# 05a一樣,# 07a這種繼承的情況也適用。
# 07a
...
item = 'xmas_card'
class ParentClass:
def func(self):
...
class Class(ParentClass, metaclass=Meta, item=item):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
方法8與方法6的Meta是一樣的,但我們另外建立了一個send_item function。
send_item接收一個參數item,並返回一個wrapper function。wrapper function接收一個參數cls,並複製所有cls的資訊加上於send_item傳入的item作為Meta的參數,來生成一個全新的class返回。# 08
from postman import postman
class Meta(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, item):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
name = 'func'
if obj := vars(cls).get(name):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
def send_item(item):
def wrapper(cls):
return Meta(cls.__name__, cls.__bases__, dict(vars(cls)), item=item)
return wrapper
item = 'xmas_card'
@send_item(item)
class Class:
def func(self):
...
if __name__ == '__main__':
inst = Class() # type(Class) => <class '__main__.Meta'>
inst.func() # item='xmas_card' is received
與# 04a與#06a一樣,# 08a這種繼承的情況也不適用。
# 08a
...
class ParentClass:
def func(self):
...
@send_item(item)
class Class(ParentClass):
...
if __name__ == '__main__':
inst = Class()
inst.func() # nothing shown on the screen
方法9與方法8類似,但我們使用getattr來尋找cls.name。
# 09
from postman import postman
class Meta(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, item):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
name = 'func'
if obj := getattr(cls, name, None):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
def send_item(item):
def wrapper(cls):
return Meta(cls.__name__, cls.__bases__, dict(vars(cls)), item=item)
return wrapper
item = 'xmas_card'
@send_item(item)
class Class:
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
與# 05a與# 07a一樣,# 09a這種繼承的情況也適用。
# 09a
...
class ParentClass:
def func(self):
...
@send_item(item)
class Class(ParentClass):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' is received
方法10直接建立一個function,接收cls及item兩個參數後,直接於function中mutate cls,重新將cls.name指定為postman(item)(obj)後,返回cls。
# 10
from postman import postman
def send_item(cls, item):
name = 'func'
if obj := vars(cls).get(name):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
class Class:
def func(self):
...
if __name__ == '__main__':
send_item(Class, 'xmas_card')
inst = Class()
inst.func() # item='xmas_card' is received
# 10a這種繼承的情況也不適用。
# 10a
...
item = 'xmas_card'
class ParentClass:
def func(self):
...
class Class(ParentClass):
...
if __name__ == '__main__':
send_item(Class, 'xmas_card')
inst = Class()
inst.func() # nothing shown on the screen
方法11與方法10類似,但我們使用getattr來尋找cls.name。
# 11
from postman import postman
def send_item(cls, item):
name = 'func'
if obj := getattr(cls, name, None):
if callable(obj):
setattr(cls, name, postman(item)(obj))
return cls
class Class:
def func(self):
...
if __name__ == '__main__':
send_item(Class, 'xmas_card')
inst = Class()
inst.func() # item='xmas_card' is received
# 11a這種繼承的情況也適用。
# 11a
...
item = 'xmas_card'
class ParentClass:
def func(self):
...
class Class(ParentClass):
...
if __name__ == '__main__':
send_item(Class, 'xmas_card')
inst = Class()
inst.func() # item='xmas_card' is received
方法12為將item設定於class body中作為class variable。
# 12
from postman import postman
class Class:
item = 'xmas_card'
@postman(item)
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' reveived
方法13為將item設定於global scope中。
# 13
from postman import postman
item = 'xmas_card'
class Class:
@postman(item)
def func(self):
...
if __name__ == '__main__':
inst = Class()
inst.func() # item='xmas_card' reveived
function中。如果是要建立一個尚不存在的function,經裝飾後再設為cls或self的attribute的話,可能會需要types.MethodType的幫忙。function的name(func)是已知的,其實我們可以考慮將需要裝飾的name(s)作為一個container收集起來,也當作參數與item一起傳遞到decorator或Meta內,再使用迴圈處理。