Сохранение подписей украшенных функций
Предположим, что я написал декоратор, который делает что-то очень общее. Например, он может преобразовывать все аргументы в определенный тип, выполнять ведение журнала, осуществлять 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 - Включить правильную подпись в docstring:
def funny_function(x, y, z=3):
"""funny_function(x, y, z=3) -- computes x*y + 2*z"""
return x*y + 2*z
Это плохо из-за дублирования. Подпись не будет отображаться должным образом в автоматически созданной документации. Легко обновить функцию и забыть об изменении docstring или опечатке. [ И да, я знаю, что 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?
Любые другие подходы?
Ответы
Ответ 1
-
Установите decorator:
$ 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
.
Ответ 2
Это решается с помощью стандартной библиотеки 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
Ответ 3
Существует декоратор-модуль с декоратором 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*z
EDIT: J. F. Sebastian отметил, что я не изменял функцию args_as_ints
- теперь он исправлен.
Ответ 4
Посмотрите на модуль decorator - в частности decorator, который решает эту проблему.
Ответ 5
Вторая опция:
$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