iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Software Development

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

[Day29] 末翼 - Term Projects:Project Postman - 研究如何傳遞decorator factory之參數

  • 分享至 

  • xImage
  •  

Postman源起

會想要研究這個小project,是因為在實作project ECC時,想要動態傳遞ttl。原先以為不太難,很快可以搞定,可是在玩了一陣子之後發現,當中有不少有趣的地方,所以想要有系統地做成筆記。

Postman目標

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

我們的目標是研究,有幾種方法可以動態傳遞itempostman,並裝飾(更精準的說法應該是apply)於# 00Classfunc上。

# 00
from postman import postman


class Class:

    def func(self):
        ...

理論上,Classfunc應該是non-data descriptor instance,但是為方便描述,我們將稱呼其為non-data descriptor

方法1:instance.__dict__

  • 由於funcnon-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
  • 由於funcnon-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.funcbound methodself會自己傳遞,所以inst.func使用起來就像是一般的instance method
  • 最後假使將funcinst.__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可能不適合這麼寫。

方法2:__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

方法3:__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:decorator(1)

方法4我們寫了一個名為decdecorator,來裝飾在Class上。

  • dec接受一個item參數後,返回一個wrapper function
  • wrapper接收一個cls參數,我們使用vars(cls).get(name)看看能不能從cls.__dict__中取到func。如果有取到的話,再進一步判斷其是否為callable。如果是的話,使用setattr重新設定cls.namepostman(item)(obj))

由於我們mutatecls,所以不管是在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)來進行搜尋,所以如# 04afunc定義於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:decorator(2)

方法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]),所以如# 05afunc定義於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:metaclass(1)

方法6使用了與方法4類似的邏輯,只是這次使用了metaclass而不是decorator。此時item需要於Class生成時,以keyword-only argument傳遞,並需要記得於Meta.__new__中加入itemsignature,才能存取到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:metaclass(2)

方法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:another class(1)

方法8方法6Meta是一樣的,但我們另外建立了一個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:another class(2)

方法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:pure function(1)

方法10直接建立一個function,接收clsitem兩個參數後,直接於functionmutate 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:pure function(2)

方法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:class body

方法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:global scope

方法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

後記

  • 這個project的目標,是希望能夠動態傳入參數至已建立的function中。如果是要建立一個尚不存在的function,經裝飾後再設為clsselfattribute的話,可能會需要types.MethodType的幫忙。
  • 這個project我們假設functionnamefunc)是已知的,其實我們可以考慮將需要裝飾的name(s)作為一個container收集起來,也當作參數與item一起傳遞到decoratorMeta內,再使用迴圈處理。

Code

本日程式碼傳送門


上一篇
[Day28] 末翼 - Term Projects:Project ECC - 建立EdgeDB Cloud Connection(2)
下一篇
[Day30] specialist與Python3.12 f-strings in the grammar
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言