【问题标题】:Python convert args to kwargsPython 将 args 转换为 kwargs
【发布时间】:2010-10-24 06:36:27
【问题描述】:

我正在编写一个装饰器,它需要在调用它正在装饰的函数之前调用其他函数。装饰函数可能有位置参数,但装饰器将调用的函数只能接受关键字参数。有没有人可以方便地将位置参数转换为关键字参数?

我知道我可以得到一个装饰函数的变量名列表:

>>> def a(one, two=2):
...    pass

>>> a.func_code.co_varnames
('one', 'two')

但我不知道如何区分按位置传入的内容以及作为关键字传入的内容。

我的装饰器看起来像这样:

class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f

    def __call__(self, *args, **kwargs):
        hozer(**kwargs)
        self.f(*args, **kwargs)

除了比较 kwargs 和 co_varnames,将不存在的任何内容添加到 kwargs 中,并希望得到最好的结果之外,还有其他方法吗?

【问题讨论】:

  • 为什么你需要知道什么是位置参数?
  • 因为我需要将它们转换为 kwargs 才能调用 hozer 函数。这个函数只接受 kwargs,但它需要知道最初调用的所有参数。因此,取决于人们是否使用位置参数或命名参数调用修饰函数,hozer 函数可能会也可能不会获得它需要的所有数据。

标签: python decorator


【解决方案1】:

任何按位置传递的 arg 都将传递给 *args。并且作为关键字传递的任何 arg 都将传递给 **kwargs。 如果您有位置参数值和名称,那么您可以这样做:

kwargs.update(dict(zip(myfunc.func_code.co_varnames, args)))

将它们全部转换为关键字参数。

【讨论】:

  • 其实kwargs.update(zip(myfunc.func_code.co_varnames, args))就够了。 dict.update 也处理 2D 可迭代对象。
  • 如果函数myfunc中有默认参数,此解决方案无法完整填写kwargs
【解决方案2】:

注意 - co_varnames 将包括局部变量和关键字。这可能无关紧要,因为 zip 会截断较短的序列,但如果您传递了错误数量的 args,可能会导致令人困惑的错误消息。

您可以使用func_code.co_varnames[:func_code.co_argcount] 避免这种情况,但更好的是使用inspect 模块。即:

import inspect
argnames, varargs, kwargs, defaults = inspect.getargspec(func)

您可能还想处理函数定义**kwargs*args 的情况(即使只是在与装饰器一起使用时引发异常)。如果设置了这些,getargspec 的第二个和第三个结果将返回它们的变量名,否则它们将为 None。

【讨论】:

    【解决方案3】:

    好吧,这可能有点矫枉过正。我为 dectools 包(在 PyPi 上)编写了它,所以你可以在那里获得更新。它返回考虑到位置、关键字和默认参数的字典。包中有一个测试套件(test_dict_as_called.py):

    def _dict_as_called(function, args, kwargs):
        """ return a dict of all the args and kwargs as the keywords they would
        be received in a real function call.  It does not call function.
        """
    
        names, args_name, kwargs_name, defaults = inspect.getargspec(function)
        
        # assign basic args
        params = {}
        if args_name:
            basic_arg_count = len(names)
            params.update(zip(names[:], args))  # zip stops at shorter sequence
            params[args_name] = args[basic_arg_count:]
        else:
            params.update(zip(names, args))    
        
        # assign kwargs given
        if kwargs_name:
            params[kwargs_name] = {}
            for kw, value in kwargs.iteritems():
                if kw in names:
                    params[kw] = value
                else:
                    params[kwargs_name][kw] = value
        else:
            params.update(kwargs)
        
        # assign defaults
        if defaults:
            for pos, value in enumerate(defaults):
                if names[-len(defaults) + pos] not in params:
                    params[names[-len(defaults) + pos]] = value
                
        # check we did it correctly.  Each param and only params are set
        assert set(params.iterkeys()) == (set(names)|set([args_name])|set([kwargs_name])
                                          )-set([None])
        
        return params
    

    【讨论】:

    • 我已经看到它被复制并粘贴到了几十个开源项目中。这应该被移到人们可以更容易调用的函数中!
    • dict.iterkeys() 方法在 Python 3 中已被弃用,所以 params.iterkeys() 只是变成了 paramsinspect.getargspec() 在更高版本的 Python 3 中也已弃用,因此请将 inspect.getargspec 调用替换为 names, args_name, kwargs_name, defaults, _kwonlyargs, _kwonlydefaults, _annotations = inspect.getfullargspec(function)
    【解决方案4】:

    如果您使用的是 Python >= 2.7,inspect.getcallargs() 会为您提供开箱即用的功能。您只需将装饰函数作为第一个参数传递给它,然后将其余参数完全按照您计划调用它的方式传递。示例:

    >>> def f(p1, p2, k1=None, k2=None, **kwargs):
    ...     pass
    >>> from inspect import getcallargs
    

    我打算做f('p1', 'p2', 'p3', k2='k2', extra='kx1')(请注意,k1 在位置上作为 p3 传递),所以...

    >>> call_args = getcallargs(f, 'p1', 'p2', 'p3', k2='k2', extra='kx1')
    >>> call_args
    {'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'kwargs': {'extra': 'kx1'}}
    

    如果你知道修饰函数不会使用**kwargs,那么那个键就不会出现在字典中,你就完成了(我假设没有*args,因为这会破坏要求所有事物都有名称)。如果你确实拥有**kwargs,就像我在这个例子中那样,并且想要将它们包含在其余的命名参数中,则需要多一行:

    >>> call_args.update(call_args.pop('kwargs'))
    >>> call_args
    {'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'extra': 'kx1'}
    

    更新:对于 Python >= 3.3,请参阅 inspect.Signature.bind() 和相关的 inspect.signature function,了解与 inspect.getcallargs() 类似(但更强大)的功能。

    【讨论】:

    • 这是正确的方法(如果你有 Python 2.7 或更高版本,几乎每个人都这样做)。
    • 查看我基于getcallargs 用法的完整答案,如下。
    【解决方案5】:

    这是使用inspect.signature 解决此问题的新方法(适用于 Python 3.3+)。我将给出一个可以自己运行/测试的示例,然后展示如何使用它来修改原始代码。

    这是一个测试函数,它只是总结了给它的任何 args/kwargs;至少需要一个参数 (a),并且有一个带有默认值 (b) 的纯关键字参数,用于测试函数签名的不同方面。

    def silly_sum(a, *args, b=1, **kwargs):
        return a + b + sum(args) + sum(kwargs.values())
    

    现在让我们为 silly_sum 创建一个包装器,它可以以与 silly_sum 相同的方式调用(除了我们将讨论的一个例外),但它只会将 kwargs 传递给包装后的 silly_sum

    def wrapper(f):
        sig = inspect.signature(f)
        def wrapped(*args, **kwargs):
            bound_args = sig.bind(*args, **kwargs)
            bound_args.apply_defaults()
            print(bound_args) # just for testing
    
            all_kwargs = bound_args.arguments
            assert len(all_kwargs.pop("args", [])) == 0
            all_kwargs.update(all_kwargs.pop("kwargs"))
            return f(**all_kwargs)
        return wrapped
    

    sig.bind 返回一个BoundArguments 对象,但这不会考虑默认值,除非您明确调用apply_defaults。如果没有给出*args/**kwargs,那么这样做也会为 args 生成一个空元组,为 kwargs 生成一个空字典。

    sum_wrapped = wrapper(silly_sum)
    sum_wrapped(1, c=9, d=11)
    # prints <BoundArguments (a=1, args=(), b=1, kwargs={'c': 9, 'd': 11})>
    # returns 22
    

    然后我们只需获取参数字典并在其中添加任何**kwargs。使用此包装器的例外是*args 不能传递给函数。这是因为这些没有名称,所以我们无法将它们转换为 kwargs。如果将它们作为名为 args 的 kwarg 传递是可以接受的,则可以改为这样做。


    这是如何将其应用于原始代码:

    import inspect
    
    
    class mydec(object):
        def __init__(self, f, *args, **kwargs):
            self.f = f
            self._f_sig = inspect.signature(f)
    
        def __call__(self, *args, **kwargs):
            bound_args = self._f_sig.bind(*args, **kwargs)
            bound_args.apply_defaults()
            all_kwargs = bound_args.arguments
            assert len(all_kwargs.pop("args"), []) == 0
            all_kwargs.update(all_kwargs.pop("kwargs"))
            hozer(**all_kwargs)
            self.f(*args, **kwargs)
    

    【讨论】:

    • 用你的包装器包装一个简单的函数,比如def _example(i, j=0): return i+jexample = wrapper(_example) 然后调用 example(1) 会引发异常。
    • 嗯,调用apply_defaults 的结果可能已更改,因此不会生成"args" 的空元组。我已经稍微更新了代码来处理这个问题。
    【解决方案6】:

    Nadia 的答案是正确的,但我觉得该答案的工作演示很有用。

    def decorator(func):
        def wrapped_func(*args, **kwargs):
            kwargs.update(zip(func.__code__.co_varnames, args))
            print(kwargs)
            return func(**kwargs)
        return wrapped_func
    
    @decorator
    def thing(a,b):
        return a+b
    

    给定这个修饰函数,以下调用会返回适当的答案:

    thing(1, 2)  # prints {'a': 1, 'b': 2}  returns 3
    thing(1, b=2)  # prints {'b': 2, 'a': 1}  returns 3
    thing(a=1, b=2)  # prints {'a': 1, 'b': 2}  returns 3
    

    但是请注意,如果您开始嵌套装饰器,事情就会变得很奇怪,因为装饰函数现在不再需要 a 和 b,而是需要 args 和 kwargs:

    @decorator
    @decorator
    def thing(a,b):
        return a+b
    

    这里thing(1,2) 将打印{'args': 1, 'kwargs': 2}TypeError: thing() got an unexpected keyword argument 'args' 错误

    【讨论】:

    • 如果被修饰的函数中有默认参数,Nadia 的回答是不正确的。它无法完全填写 kwargs,例如装饰def thing(a,b=5) 然后调用thing(1) kwargs 变为{'a': 1} 而不是{'a': 1, 'b': 5}
    【解决方案7】:

    @mikenerone 提出的(最佳)解决方案是原始发帖人问题的解决方案:

    import inspect
    from functools import wraps
    
    class mydec(object):
        def __init__(self, f, *args, **kwargs):
            self.f = f
    
        def __call__(self, *args, **kwargs):
            call_args = inspect.getcallargs(self.f, *args, **kwargs)
            hozer(**call_args)
    
            return self.f(*args, **kwargs)
    
    def hozer(**kwargs):
        print('hozer got kwds:', kwargs)
    
    def myadd(i, j=0):
        return i + j
    
    o = mydec(myadd)
    assert o(1,2) == 3
    assert o(1) == 1
    assert o(1, j=2) == 3
    
    hozer got kwds: {'i': 1, 'j': 2}
    hozer got kwds: {'i': 1, 'j': 0}
    hozer got kwds: {'i': 1, 'j': 2}
    

    这是一个通用装饰器,它将 Python 函数的所有参数转换并合并为 kwargs,并仅使用这些 kwargs 调用包装函数。

    def call_via_kwargs(f):
        @wraps(f)
        def wrapper(*args, **kwds):
            call_args = inspect.getcallargs(f, *args, **kwds)
            print('kwargs:', call_args)
            return f(**call_args)
        return wrapper
    
    
    @call_via_kwargs
    def adder(i, j=0):
        return i + j
    
    assert adder(1) == 1
    assert adder(i=1) == 1
    assert adder(1, j=2) == 3
    
    kwargs: {'i': 1, 'j': 0}
    kwargs: {'i': 1, 'j': 0}
    kwargs: {'i': 1, 'j': 2}
    

    这些解决方案正确处理默认参数。

    【讨论】:

      猜你喜欢
      • 2011-08-08
      • 2020-09-16
      • 2017-01-28
      • 2013-03-07
      • 1970-01-01
      • 2018-01-08
      • 2018-08-28
      • 2023-03-19
      相关资源
      最近更新 更多