简体   繁体   English

Python 将 args 转换为 kwargs

[英]Python convert args to kwargs

I am writing a decorator that needs to call other functions prior to call of the function that it is decorating.我正在编写一个装饰器,它需要在调用它正在装饰的函数之前调用其他函数。 The decorated function may have positional arguments, but the functions the decorator will call can only accept keyword arguments.装饰函数可能有位置参数,但装饰器将调用的函数只能接受关键字参数。 Does anyone have a handy way of converting positional arguments into keyword arguments?有没有人有将位置参数转换为关键字参数的方便方法?

I know that I can get a list of the variable names of the decorated function:我知道我可以获得修饰函数的变量名称列表:

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

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

But I can't figure out how to tell what was passed in positionally and what was as keyword.但是我不知道如何分辨什么是位置传递的,什么是关键字。

My decorator looks like this:我的装饰器看起来像这样:

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

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

Is there a way other than just comparing kwargs and co_varnames, adding to kwargs anything not in there, and hoping for the best?除了比较 kwargs 和 co_varnames 之外,还有什么方法可以向 kwargs 添加任何不在那里的东西,并希望得到最好的结果?

Any arg that was passed positionally will be passed to *args.任何按位置传递的 arg 都将传递给 *args。 And any arg passed as a keyword will be passed to **kwargs.任何作为关键字传递的 arg 都将传递给 **kwargs。 If you have positional args values and names then you can do:如果您有位置参数值和名称,则可以执行以下操作:

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

to convert them all into keyword args.将它们全部转换为关键字参数。

If you're using Python >= 2.7 inspect.getcallargs() does this for you out of the box.如果您使用 Python >= 2.7 inspect.getcallargs()为您开箱即用。 You'd just pass it the decorated function as the first argument, and then the rest of the arguments exactly as you plan to call it.您只需将装饰函数作为第一个参数传递给它,然后将其余参数完全按照您计划调用它的方式传递。 Example:例子:

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

I'm planning to do f('p1', 'p2', 'p3', k2='k2', extra='kx1') (note that k1 is being passed positionally as p3), so...我打算做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'}}

If you know the decorated function won't use **kwargs , then that key won't appear in the dict, and you're done (and I'm assuming there's no *args , since that would break the requirement that everything have a name).如果您知道装饰函数不会使用**kwargs ,那么该键将不会出现在 dict 中,您就完成了(我假设没有*args ,因为这会打破一切都具有的要求一个名字)。 If you do have **kwargs , as I have in this example, and want to include them with the rest of the named arguments, it takes one more line:如果你确实**kwargs ,就像我在这个例子中那样,并且想要将它们与其余的命名参数一起包含,则需要多一行:

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

Update: for Python >= 3.3, see inspect.Signature.bind() and the related inspect.signature function for functionality similar to (but more robust than) inspect.getcallargs() .更新:对于 Python >= 3.3,请参阅inspect.Signature.bind()和相关的inspect.signature函数以了解类似于(但比) inspect.getcallargs()

Note - co_varnames will include local variables as well as keywords.注意 - co_varnames 将包括局部变量和关键字。 This probably won't matter, as zip truncates the shorter sequence, but may result in confusing error messages if you pass the wrong number of args.这可能无关紧要,因为 zip 会截断较短的序列,但如果您传递了错误数量的 args,则可能会导致混淆错误消息。

You can avoid this with func_code.co_varnames[:func_code.co_argcount] , but better is to use the inspect module.您可以使用func_code.co_varnames[:func_code.co_argcount]避免这种情况,但最好使用检查模块。 ie: IE:

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

You may also want to handle the case where the function defines **kwargs or *args (even if just to raise an exception when used with the decorator).您可能还想处理函数定义**kwargs*args (即使只是在与装饰器一起使用时引发异常)。 If these are set, the second and third result from getargspec will return their variable name, otherwise they will be None.如果设置了这些,则getargspec的第二个和第三个结果将返回它们的变量名称,否则它们将为 None。

Well, this may be overkill.嗯,这可能有点矫枉过正。 I wrote it for the dectools package (on PyPi), so you can get updates there.我是为 dectools 包(在 PyPi 上)编写的,所以你可以在那里获得更新。 It returns the dictionary taking into account positional, keyword, and default arguments.它返回考虑位置、关键字和默认参数的字典。 There is a test suite in the package (test_dict_as_called.py):包中有一个测试套件(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

Nadia's answer is correct, but I feel like a working demo of that answer is useful. 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

Given this decorated function, the following calls return the appropriate answer:鉴于此修饰函数,以下调用将返回适当的答案:

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

Note however that things start getting weird if you start nesting decorators because the decorated function now no longer takes a and b, it takes args and kwargs:但是请注意,如果您开始嵌套装饰器,事情会变得很奇怪,因为装饰函数现在不再需要 a 和 b,而是需要 args 和 kwargs:

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

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

Here's a newer method to solve this using inspect.signature (for Python 3.3+).这是使用inspect.signature (适用于Python 3.3+)解决此问题的较新方法。 I'll give an example that can be run / tested yourself first and then show how to modify the original code with it.我先举一个可以自己运行/测试的例子,然后展示如何用它修改原始代码。

Here's a test function which just sums up any args/kwargs given to it;这是一个测试函数,它总结了给它的任何 args/kwargs; at least one argument is required ( a ) and there's one keyword-only argument with a default value ( b ), just to test different aspects of function signatures.至少需要一个参数 ( a ) 并且有一个带有默认值 ( b ) 的仅关键字参数,只是为了测试函数签名的不同方面。

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

Now let's make a wrapper for silly_sum which can be called in the same way as silly_sum (with an exception which we'll get to) but that only passes in kwargs to the wrapped silly_sum .现在让我们为silly_sum制作一个包装器,它可以以与silly_sum相同的方式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 returns a BoundArguments object, but this doesn't take defaults into account unless you call apply_defaults explicitly. sig.bind返回一个BoundArguments对象,但这不会考虑默认值,除非您显式调用apply_defaults Doing so will also generate an empty tuple for args and an empty dict for kwargs if no *args / **kwargs were given.如果没有给出*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

Then we just get the dictionary of arguments and add any **kwargs in. The exception to using this wrapper is that *args can't be passed to the function.然后我们只需获取参数字典并添加任何**kwargs 。使用此包装器的例外是*args不能传递给函数。 This is because there are no names for these, so we can't convert them into kwargs.这是因为这些没有名称,所以我们不能将它们转换为 kwargs。 If passing them through as a kwarg named args is acceptable, that could be done instead.如果将它们作为名为 args 的 kwarg 传递是可以接受的,那么可以改为这样做。


Here is how this can be applied to the original code:以下是如何将其应用于原始代码:

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)

Fleshing out the (best) solution by @mikenerone here is the solution to original poster's problem: @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}

Here is a generalised decorator that converts and consolidates all parameters to a Python function into kwargs and calls the wrapped function only with those kwargs.这是一个通用装饰器,它将 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}

These solutions correctly handle default parameters.这些解决方案正确处理默认参数。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM