Сохранение подписей декорированных функций - PullRequest
96 голосов
/ 29 сентября 2008

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

Вот пример:

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?

Есть ли другие подходы?

Ответы [ 6 ]

70 голосов
/ 29 сентября 2008
  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() из 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.

13 голосов
/ 21 июля 2015

Это решается с помощью стандартной библиотеки 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
9 голосов
/ 29 сентября 2008

Существует модуль декоратора с 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 - она ​​исправлена.

8 голосов
/ 29 сентября 2008

Взгляните на модуль decorator - в частности, decorator decorator, который решает эту проблему.

6 голосов
/ 28 августа 2014

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

  1. Установить упакованный модуль:

$ easy_install wrapt

Обернуть есть бонус, сохранить подпись класса.


import wrapt
import inspect</p>

@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
1 голос
/ 14 марта 2019

Как указано выше в ответе 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.

...