Сохранение сигнатур декорированных функций
предположим, я написал декоратор, который делает что-то очень обобщенное. Например, он может преобразовать все аргументы в определенный тип, выполнить ведение журнала, реализовать memoization и т. д.
вот пример:
def args_as_ints(f):
def g(*args, **kwargs):
args = [int(x) for x in args]
kwargs = dict((k, int(v)) for k, v in kwargs.items())
return f(*args, **kwargs)
return g
@args_as_ints
def funny_function(x, y, z=3):
"""Computes x*y + 2*z"""
return x*y + 2*z
>>> funny_function("3", 4.0, z="5")
22
все хорошо до сих пор. Однако есть одна проблема. Оформленная функция не сохраняет документацию исходной функции:
>>> help(funny_function)
Help on function g in module __main__:
g(*args, **kwargs)
к счастью, есть обходной путь:
def args_as_ints(f):
def g(*args, **kwargs):
args = [int(x) for x in args]
kwargs = dict((k, int(v)) for k, v in kwargs.items())
return f(*args, **kwargs)
g.__name__ = f.__name__
g.__doc__ = f.__doc__
return g
@args_as_ints
def funny_function(x, y, z=3):
"""Computes x*y + 2*z"""
return x*y + 2*z
на этот раз, имя функции и документация верны:
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(*args, **kwargs)
Computes x*y + 2*z
но есть еще проблема: сигнатура функции неверна. Информация "* args, **kwargs" почти бесполезна.
что делать? Я могу думать о двух простых, но ошибочных обходных путях:
1 -- Включите правильную подпись в строку документа:
def funny_function(x, y, z=3):
"""funny_function(x, y, z=3) -- computes x*y + 2*z"""
return x*y + 2*z
это плохо из-за дублирования. Подпись по-прежнему не будет отображаться должным образом в автоматически сгенерированной документации. Это легко обновить функцию и забыть об изменении строки документа, или сделать опечатку. [и да, я знаю о том, что docstring уже дублирует тело функции. Пожалуйста, игнорируйте это; funny_function - это просто случайный пример.]
2 -- не использовать декоратор, или использовать специальный декоратор для каждой конкретной подписи:
def funny_functions_decorator(f):
def g(x, y, z=3):
return f(int(x), int(y), z=int(z))
g.__name__ = f.__name__
g.__doc__ = f.__doc__
return g
это прекрасно работает для набора функций, которые имеют идентичную подпись, но это бесполезно в целом. как я сказал в начале, я хочу иметь возможность использовать декораторы полностью в общем виде.
Я ищу решение, которое является полностью общим и автоматическим.
Итак, вопрос: есть ли способ изменить подпись украшенной функции после ее создания?
в противном случае, могу ли я написать декоратор, который извлекает сигнатуру функции и использует эту информацию вместо "*kwargs, **kwargs" при построении украшенной функции? Как мне извлечь это информация? Как я должен построить украшенную функцию -- с exec?
какие-то другие подходы?
5 ответов:
установить оформителя модуль:
$ pip install decoratorопределение адаптации
args_as_ints():import decorator @decorator.decorator def args_as_ints(f, *args, **kwargs): args = [int(x) for x in args] kwargs = dict((k, int(v)) for k, v in kwargs.items()) return f(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x*y + 2*z print funny_function("3", 4.0, z="5") # 22 help(funny_function) # Help on function funny_function in module __main__: # # funny_function(x, y, z=3) # Computes x*y + 2*z
Python 3.4+
functools.wraps()из stdlib сохраняет подписи с Python 3.4:import functools def args_as_ints(func): @functools.wraps(func) def wrapper(*args, **kwargs): args = [int(x) for x in args] kwargs = dict((k, int(v)) for k, v in kwargs.items()) return func(*args, **kwargs) return wrapper @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x*y + 2*z print(funny_function("3", 4.0, z="5")) # 22 help(funny_function) # Help on function funny_function in module __main__: # # funny_function(x, y, z=3) # Computes x*y + 2*z
functools.wraps()доступна по крайней мере, с Python 2.5 но он не сохраняет подпись там:help(funny_function) # Help on function funny_function in module __main__: # # funny_function(*args, **kwargs) # Computes x*y + 2*zуведомления:
*args, **kwargsвместоx, y, z=3.
это решается с помощью стандартной библиотеки Python
functoolsи в частностиfunctools.wrapsфункция, которая предназначена для "обновите функцию-оболочку, чтобы она выглядела как обернутая функция". Однако его поведение зависит от версии Python, как показано ниже. Применительно к примеру из вопроса, код будет выглядеть так:from functools import wraps def args_as_ints(f): @wraps(f) def g(*args, **kwargs): args = [int(x) for x in args] kwargs = dict((k, int(v)) for k, v in kwargs.items()) return f(*args, **kwargs) return g @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x*y + 2*zпри выполнении в Python 3, это приведет к следующему:
>>> funny_function("3", 4.0, z="5") 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*zего единственным недостатком является то, что в Однако Python 2 не обновляет список аргументов функции. При выполнении в Python 2, он будет производить:
>>> help(funny_function) Help on function funny_function in module __main__: funny_function(*args, **kwargs) Computes x*y + 2*z
есть модуль декоратор С
decoratorдекоратор вы можете использовать:@decorator def args_as_ints(f, *args, **kwargs): args = [int(x) for x in args] kwargs = dict((k, int(v)) for k, v in kwargs.items()) return f(*args, **kwargs)тогда подпись и справка метода сохраняются:
>>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*zEDIT: J. F. Себастьян указал, что я не изменял
args_as_intsфункция -- теперь она исправлена.
посмотри оформителя модуль - а конкретно оформителя оформителя, который решает эту проблему.
второй вариант:
- установите модуль wrapt:
$ easy_install wrapt
wrapt есть бонус, сохранить подпись класса.
import wrapt import inspect @wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, z=5)) # 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*z
Comments