Сохранение сигнатур декорированных функций
Вопрос
Предположим, я написал декоратор, который делает что-то очень общее.Например, он может преобразовать все аргументы в определенный тип, выполнить журналирование, реализовать запоминание и т. д.
Вот пример:
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
Это плохо из-за дублирования.Подпись по-прежнему не будет отображаться должным образом в автоматически созданной документации.Легко обновить функцию и забыть об изменении строки документации или допущении опечатки.[И да, я осознаю тот факт, что строка документации уже дублирует тело функции.Пожалуйста, игнорируйте это;забавная_функция — это всего лишь случайный пример.]
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?
Есть ли другие подходы?
Решение
Установить декоратор модуль:
$ 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
Питон 3.4+
functools.wraps()
из стандартной библиотеки сохраняет подписи начиная с 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
Eсть модуль декоратора с 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
РЕДАКТИРОВАТЬ:Дж.Ф.Себастьян отметил, что я не менял args_as_ints
функция - теперь она исправлена.
Второй вариант:
- Установите модуль обертки:
$ easy_install обертка
У обертки есть бонус: сохраните сигнатуру класса.
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
Как прокомментировано выше в ответ jfs ;если вас волнует подпись с точки зрения внешнего вида (help
, и inspect.signature
), затем используя functools.wraps
все в порядке.
Если вас беспокоит подпись с точки зрения поведения (в частности TypeError
в случае несовпадения аргументов), functools.wraps
не сохраняет его.Вам лучше использовать decorator
для этого или моего обобщения его основного механизма, названного makefun
.
from makefun import wraps
def args_as_ints(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("wrapper executes")
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"))
# wrapper executes
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
# Computes x*y + 2*z
funny_function(0)
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)
Смотрите также этот пост о functools.wraps
.