Python конвертирует args в kwargs - PullRequest
17 голосов
/ 06 мая 2009

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

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

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

Ответы [ 5 ]

17 голосов
/ 06 мая 2009

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

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

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

11 голосов
/ 23 октября 2013

Если вы используете 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 нет, так как это сломало бы требование, чтобы у всего было имя). Если у вас do есть **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().

6 голосов
/ 07 мая 2009

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

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

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

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

5 голосов
/ 22 февраля 2010

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

Вот новый способ решения этой проблемы с использованием 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 (с исключением, к которому мы вернемся), но который передается только в 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 явно. Это также приведет к созданию пустого кортежа для аргументов и пустого слова для 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 in. Исключением из использования этой оболочки является то, что *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)
...