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


person jkoelker    schedule 06.05.2009    source источник
comment
Зачем вам нужно знать, какие аргументы были позиционными?   -  person Jason Coon    schedule 06.05.2009
comment
Потому что мне нужно преобразовать их в kwargs, чтобы вызвать функцию hozer. Эта функция принимает только kwargs, но ей нужно знать обо всех первоначально вызванных аргументах. Таким образом, в зависимости от того, вызывают ли люди декорированную функцию с позиционными или именованными аргументами, функция hozer может получить или не получить все необходимые ей данные.   -  person jkoelker    schedule 06.05.2009


Ответы (7)


Примечание. 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.

person Brian    schedule 07.05.2009

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

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

чтобы преобразовать их все в аргументы ключевого слова.

person Nadia Alramli    schedule 06.05.2009
comment
На самом деле kwargs.update(zip(myfunc.func_code.co_varnames, args)) достаточно. dict.update также обрабатывает 2D-итерации. - person obskyr; 25.11.2016
comment
Это решение не может полностью заполнить kwargs, если в функции myfunc есть параметры по умолчанию. - person abulka; 09.06.2021

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

person mikenerone    schedule 23.10.2013
comment
Это правильный способ сделать это (если у вас Python 2.7 или новее, что есть практически у всех). - person John Zwinck; 22.05.2017
comment
См. мой полностью конкретизированный ответ, основанный на этом использовании getcallargs, ниже. - person abulka; 09.06.2021

Ну, это может быть лишним. Я написал его для пакета 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
person Charles Merriam    schedule 22.02.2010
comment
Я видел, как это копировали и вставляли в десятки проектов с открытым исходным кодом. Это должно быть перемещено в функцию, которую людям будет проще вызывать! - person Erik Aronesty; 14.12.2017
comment
Метод dict.iterkeys() устарел в Python 3, поэтому params.iterkeys() просто становится params. inspect.getargspec() также не рекомендуется в более поздних версиях Python 3, поэтому замените вызов inspect.getargspec на names, args_name, kwargs_name, defaults, _kwonlyargs, _kwonlydefaults, _annotations = inspect.getfullargspec(function). - person abulka; 09.06.2021

Ответ Нади правильный, но я чувствую, что рабочая демонстрация этого ответа полезна.

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, она принимает аргументы и 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'

person JnBrymn    schedule 27.10.2019
comment
Ответ Нади неверен, если в декорируемой функции есть параметры по умолчанию. Он не может полностью заполнить kwargs, например. украшая def thing(a,b=5), затем вызывая вещь (1) kwargs, становится {'a': 1}, а не {'a': 1, 'b': 5}. - person abulka; 09.06.2021

Вот более новый метод решения этой проблемы с использованием 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)
person Nathan    schedule 04.04.2019
comment
Обертывание простой функции, такой как def _example(i, j=0): return i+j, вашей оболочкой, например. example = wrapper(_example), то вызов example(1) вызывает исключение. - person abulka; 09.06.2021
comment
Хм, результаты вызова apply_defaults могли измениться, так что пустой кортеж для "args" не генерируется. Я немного обновил код, чтобы справиться с этим. - person Nathan; 09.06.2021

Изложение (лучшее) решение @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}

Эти решения правильно обрабатывают параметры по умолчанию.

person abulka    schedule 09.06.2021