Вопрос

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

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

>>> 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)
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top