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 чего-либо, чего нет, и надежды на лучшее?
Решение
Примечание. В 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.
Другие советы
Любой аргумент, который был передан позиционно, будет передан * args. И любой аргумент, переданный в качестве ключевого слова, будет передан ** kwargs. Если у вас есть значения и имена позиционных аргументов, вы можете сделать:
kwargs.update(dict(zip(myfunc.func_code.co_varnames, args)))
чтобы преобразовать их все в аргументы ключевых слов.
Если вы используете 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 передается позиционно как р3), так что ...
>>> 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 см. < code> inspect.Signature.bind () и связанные с ним inspect.signature
function для функциональности, аналогичной (но более надежной, чем) inspect.getcallargs ()
.
Ну, это может быть излишним. Я написал его для пакета 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
Вот более новый метод решения этой проблемы с использованием 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
явно. Это также создаст пустой кортеж для args и пустой dict для 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. Если приемлемо передавать их в виде 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)