簡體   English   中英

我可以使用 inspect.signature 將裝飾器的 (*args, **kwargs) 解釋為裝飾函數的變量嗎?

[英]Can I use inspect.signature to interpret a decorator's (*args, **kwargs) as the decorated function's variables would be?

而是一個棘手的問題。 我希望我做得很好。 為了澄清,這里是一個示例裝飾器:

class my_decorator:
    def __init__(self, option=None):
        self.option = option

    def __call__(self, fn):
        @functools.wraps(fn)
        def decorated(*args, **kwargs):
            fn_args = inspect.signature(fn).parameters
            
            # Code that creates local variables by taking the values that
            # arrived in *args and **kwargs in exactly the same way that
            # they are inside fn proper.

            return fn(*args, **kwargs)

        return decorated

如果還不清楚,我們可以通過查看所述裝飾器的應用程序來為提案添加解釋性細節:

@my_decorator()
def my_function(arg1, arg2, arg3, kwarg1=val1, kwarg2=val2)
    # In here we see the variables arg1, arg2, arg3, kwarg1 and kwarg2

挑戰在於,我們能否在def decorated(*args, **kwargs):中使用fn_args創建變量arg1arg2arg3kwarg1kwarg2

我有直覺認為這是可以做到的,但這需要一些深入的思考,我想知道這是否不是已經發明的輪子。 我的意思是,也許已經有一種規范的方法可以將函數簽名應用於匿名 *args、**kwargs 以生成與該簽名一致的局部變量。

我願意這樣想。 也就是說,我不是第一個發現這種願望/願望的人。 當然,我有一個真正的風險。 因為這對我來說一般效用似乎很低。 我有一個非常具體的應用程序,其中傳入的option是格式字符串(樣式為“{arg1},{arg2} ...”),它使用修飾函數的參數列表中的名稱。

好吧,研究現有解決方案的大量努力沒有結果,所以我制定了一個可行的解決方案,盡管我不是 100% 滿意(因為我正在使用 'exec' 這實際上是可選的,主要技巧inspect.signature.parameters幫助我們在裝飾函數的期望上下文中解釋傳入參數):

from inspect import signature

... then inside 'decorated' ...

allargs = signature(fn).parameters

# allargs is an OrderedDict, the keys of which are the arguments of fn.
# We build therfrom, a list of arguments we are seeking in args and kwargs
seeking = list(allargs.keys())
found = {}

# Methods by convention have the first argument "self". Even if it's
# not a method, setting a local variable 'self' is fraught with issues
# And so we need to remap it. Also if we've explicitly decorated a method
# we remap the first argument. We call it 'selfie' interanally, but in
# provided key_patterns, accept 'self' as a reference.
if seeking[0] == 'self' or is_method:
    selfie = args[0]
    found['selfie'] = selfie
    seeking.pop(0)
    sarg = 1
    is_method = True
else:
    sarg = 0

# For classifying arguments see:
# https://docs.python.org/3/library/inspect.html#inspect.Parameter
#
# We start by consuming all the args.
if seeking:
    for arg in args[sarg:]:
        # This should never happen, but if someone calls the decorated function
        # with more args than the original function can accept that's clearly
        # an erroneous call.
        if len(seeking) == 0:
            raise TooManyArgs(f"Decorated function has been called with {len(args)} positional arguments when only {len(allargs)} args are accepted by the decorated function. ")

        # Set a local variable, feigning the conditions that fn would see
        # if seeking[0] is 'self' this exhibits odd dysfunctional behaviour
        # and so above we mapped 'self' to 'selfie' internall of this decorator.
        found[seeking[0]] = arg
        exec(f"{seeking[0]} = arg")
        seeking.pop(0)

    # If we did not find all that we seek by consuming args, consume kwargs
    if seeking:
        for kwarg, val in kwargs.items():
            if kwarg in seeking:
                # Should never happen, but if someone calls the decorated function
                # with more args than the original function can accept that's clearly
                # an erroneous call.
                if len(seeking) == 0:
                    raise TooManyKwargs(f"Decorated function has been called with {len(kwargs)} keyword arguments after {len(args)} positional arguments when only {len(allargs)} args are accepted by the decorated function. ")

                arg = seeking.index(kwarg)
                found[seeking[arg]] = val
                exec(f"{seeking[arg]} = val")
                seeking.pop(arg)

        if seeking:
            # Any that remain we can check for default values
            for arg in seeking:
                props = allargs[arg]
                if props.default != props.empty:
                    pos = seeking.index(arg)
                    found[seeking[pos]] = props.default
                    exec(f"{seeking[pos]} = props.default")
                    seeking.pop(pos)

            # If any remain, then clearly not all the argument fn needs have been supplied
            # to its decorated version.
            if seeking:
                raise TooFewArgs(f"Decorated function expects arguments ({', '.join(seeking)}), which it was not called with.")

這需要一些自定義異常:

class TooManyArgs(Exception):
    pass

class TooManyKwargs(Exception):
    pass

class TooFewArgs(Exception):
    pass

但工作正常。 結果是decorated中的局部變量,就像它們在裝飾函數中看到的一樣。

雖然現場需要,但在這里使用它:

它在這里使用:

https://pypi.org/project/django-cache-memoized/

並在這里采取行動:

https://github.com/bernd-wechner/django-cache-memoized/blob/master/src/django_cache_memoized/__init__.py

(我們希望結束一個非常明智、經過充分研究的問題的似是而非的投票不會找到 2 個支持者。恕我直言,這是一個非常有趣的問題,而且似乎可行,盡管首選規范方法。)

正如評論中的 user2357112 所述,您可以使用 inspect.signature.bind 獲取參數和關鍵字參數的字典。

你的裝飾器可能是這樣的:

class my_decorator:
    def __init__(self, option=None):
        self.option = option

    def __call__(self, fn):
        @functools.wraps(fn)
        def decorated(*args, **kwargs):
            fn_args = inspect.signature(fn).bind(*args, **kwargs)
            
            # Then if you really needed to make them as variables 
            # rather than a dict for a reason I can't imagine
            locals.update(fn_args)

            return fn(*args, **kwargs)

        return decorated

如果像我一樣你需要保持 python 2.7 和 python 3 的兼容性,有一個類似的函數inspect.getcallargs對兩者都適用,即使它在 3.5 后已被棄用

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM