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

Предположим, что я написал декоратор, который делает что-то очень общее. Например, он может преобразовывать все аргументы в определенный тип, выполнять ведение журнала, осуществлять 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

Вторая опция:

  • Установить модуль 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