Подтвердить что ты не робот

Python конвертирует args в kwargs

Я пишу декоратор, которому нужно вызвать другие функции до вызова функции, которую она украшает. Декорированная функция может иметь позиционные аргументы, но функции, которые вызовет декоратор, могут принимать только аргументы ключевых слов. Кто-нибудь имеет удобный способ преобразования позиционных аргументов в аргументы ключевых слов?

Я знаю, что могу получить список имен переменных украшенной функции:

>>> 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 ничего, что не там, и надеясь на лучшее?

4b9b3361

Ответ 1

Примечание - co_varnames будет включать локальные переменные, а также ключевые слова. Это, вероятно, не будет иметь значения, поскольку zip усекает более короткую последовательность, но может привести к сбивающим с толку сообщениям об ошибках, если вы передадите неверное количество аргументов.

Вы можете избежать этого с помощью func_code.co_varnames[:func_code.co_argcount], но лучше использовать модуль inspect. то есть:

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

Вы также можете захотеть обработать случай, когда функция определяет **kwargs или *args (даже если просто вызвать исключение при использовании с декоратором). Если они установлены, второй и третий результат getargspec вернет имя их переменной, в противном случае они будут getargspec None.

Ответ 2

Любой аргумент, который был передан позиционно, будет передан в * args. И любой аргумент, переданный как ключевое слово, будет передан ** kwargs. Если у вас есть позиционные значения и имена args, вы можете сделать следующее:

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

чтобы преобразовать их все в ключевые слова args.

Ответ 3

Если вы используете 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, то этот ключ не появится в dict, и все готово (и я предполагаю, что нет *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 для функциональности, аналогичной (но более надежной, чем) inspect.getcallargs().

Ответ 4

Хорошо, это может быть излишним. Я написал его для пакета 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

Ответ 5

Здесь новый метод для решения этой проблемы с помощью inspect.signature (для Python 3. 3+). Я приведу пример, который можно сначала запустить/протестировать самостоятельно, а затем покажу, как с его помощью изменить исходный код.

Здесь тестовая функция, которая просто суммирует любые аргументы /kwargs, данные ей; требуется по крайней мере один аргумент (a) и один аргумент только для ключевого слова со значением по умолчанию (b), просто для проверки включите различные аспекты сигнатур функций.

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

Теперь давайте 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 возвращает объект BoundArguments, но это не учитывает значения по умолчанию, если вы не apply_defaults явно. Это также сгенерирует пустой кортеж для args и пустой dict для kwargs, если args/kwargs не было задано.

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

Затем мы просто получаем словарь аргументов и добавляем любые **kwargs. Исключением из использования этой оболочки является то, что *args не могут быть переданы функции. Это потому, что для них нет названий, поэтому мы не можем конвертировать их в kwargs. Если приемлемо передавать их как kwarg с именем args, это можно сделать вместо этого.


Вот как это можно применить к исходному коду:

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)