Сохранение подписей декорированных функций

111

Предположим, я написал декоратор, который делает что-то очень общее. Например, он может преобразовывать все аргументы в определенный тип, вести журнал, реализовывать мемоизацию и т. Д.

Вот пример:

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

Это плохо из-за дублирования. Подпись по-прежнему не будет отображаться должным образом в автоматически созданной документации. Функцию легко обновить и забыть об изменении строки документации или сделать опечатку. [ И да, я знаю, что строка документации уже дублирует тело функции. Пожалуйста, проигнорируйте это; 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
Никогда не говорил «устарело». Мне было более или менее интересно, что inspect.Signatureдобавилось к украшенным функциям.
NightShadeQueen

Ответы:

79
  1. Установите модуль декоратора :

    $ pip install decorator
  2. Адаптировать определение 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()from 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.

jfs
источник
Ваш ответ не был первым, но пока самым исчерпывающим :-) Я бы предпочел решение, не использующее сторонний модуль, но глядя на источник модуля декоратора, оно достаточно простое, чтобы я мог просто скопируйте это.
Фредрик Йоханссон,
1
@MarkLodato: functools.wraps()уже сохраняет подписи в Python 3.4+ (как сказано в ответе). Вы имеете в виду, что настройка wrapper.__signature__помогает в более ранних версиях? (какие версии вы тестировали?)
jfs
1
@MarkLodato: help()показывает правильную подпись на Python 3.4. Как вы думаете, почему functools.wraps()не работает IPython?
jfs
1
@MarkLodato: он не работает, если нам нужно написать код для его исправления. Учитывая, что это help()дает правильный результат, вопрос в том, какое программное обеспечение следует исправить: functools.wraps()или IPython? В любом случае назначение вручную __signature__- это в лучшем случае обходной путь, а не долгосрочное решение.
jfs
1
Похоже, inspect.getfullargspec()все еще не возвращает правильную подпись functools.wrapsв python 3.4, которую вы должны использовать inspect.signature()вместо этого.
Туукка Мустонен
16

Это решается с помощью стандартной библиотеки 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
Тимур
источник
Не уверен, что это Sphinx, но, похоже, это не работает, когда обернутая функция является методом класса. Sphinx продолжает сообщать подпись вызова декоратора.
алфавитасуп
9

Есть модуль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

РЕДАКТИРОВАТЬ: JF Себастьян указал, что я не изменял args_as_intsфункцию - теперь она исправлена.

DzinX
источник
6

Второй вариант:

  1. Установите модуль wrapt:

$ easy_install в оболочке

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
макм
источник
2

Как прокомментировано выше в ответе 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 .

умница
источник
1
Также inspect.getfullargspecне сохраняется результат по вызову functools.wraps.
laike9m
Спасибо за полезный дополнительный комментарий @ laike9m!
Смари