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

StackOverflow https://stackoverflow.com/questions/147816

  •  02-07-2019
  •  | 
  •  

Вопрос

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

Вот пример:

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?

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

Это было полезно?

Решение

  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
    

Питон 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 функция - теперь она исправлена.

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

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

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

$ 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.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top