【问题标题】:Can I use inspect.signature to interpret a decorator's (*args, **kwargs) as the decorated function's variables would be?我可以使用 inspect.signature 将装饰器的 (*args, **kwargs) 解释为装饰函数的变量吗?
【发布时间】:2022-06-11 17:42:44
【问题描述】:

这是一个相当棘手的问题。我希望我做得很好。为了澄清,这里是一个示例装饰器:

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

挑战在于,我们能否使用fn_argsdef decorated(*args, **kwargs): 内部创建变量arg1arg2arg3kwarg1kwarg2

我有直觉认为这是可以做到的,但这需要一些深入的思考,我想知道这不是已经发明的轮子。我的意思是,也许已经有一种规范的方法可以将函数签名应用于匿名 *args、**kwargs 以生成与该签名一致的局部变量。

我愿意这样想。也就是说,我不是第一个发现这种愿望/欲望的人。当然,我有一个真正的风险。因为这对我来说一般效用似乎很低。我有一个非常具体的应用程序,其中传入的option 是一个格式字符串(样式为“{arg1},{arg2} ...”),它使用修饰函数的参数列表中的名称。

【问题讨论】:

    标签: python python-3.x python-decorators signature


    【解决方案1】:

    好吧,研究现有解决方案的大量努力没有结果,所以我制定了一个可行的解决方案,尽管我不是 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 个支持者。恕我直言,这是一个非常有趣的问题,并且似乎可行,尽管首选规范方法。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-08-28
      • 2013-09-25
      • 1970-01-01
      • 2016-04-20
      • 2020-05-20
      • 2023-03-17
      • 1970-01-01
      • 2017-01-28
      相关资源
      最近更新 更多