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



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



def funny_function(x, y, z=3):
"""funny_function(x, y, z=3) -- computes x*y + 2*z"""
return x*y + 2*z


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



какие-то другие подходы?

619   5  

5 ответов:

  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.

это решается с помощью стандартной библиотеки 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

есть модуль декоратор С 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. Себастьян указал, что я не изменял args_as_ints функция -- теперь она исправлена.

посмотри оформителя модуль - а конкретно оформителя оформителя, который решает эту проблему.

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

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

Comments

    Ничего не найдено.