Что делает functools.wraps?

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


person Eli Courtwright    schedule 21.11.2008    source источник


Ответы (6)


Когда вы используете декоратор, вы заменяете одну функцию другой. Другими словами, если у вас есть декоратор

def logged(func):
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

тогда, когда ты скажешь

@logged
def f(x):
   """does some math"""
   return x + x * x

это в точности то же самое, что и сказать

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)

и ваша функция f заменяется функцией with_logging. К сожалению, это означает, что если вы затем скажете

print(f.__name__)

он напечатает with_logging, потому что это имя вашей новой функции. Фактически, если вы посмотрите на строку документации для f, она будет пустой, потому что with_logging не имеет строки документации, и поэтому написанная вами строка документации больше не будет там. Кроме того, если вы посмотрите на результат pydoc для этой функции, он не будет указан как принимающий один аргумент x; вместо этого он будет указан как принимающий *args и **kwargs, потому что это то, что принимает with_logging.

Если бы использование декоратора всегда означало потерю этой информации о функции, это было бы серьезной проблемой. Вот почему у нас есть functools.wraps. Это берет функцию, используемую в декораторе, и добавляет функциональность копирования имени функции, строки документации, списка аргументов и т. Д. А поскольку wraps сам является декоратором, следующий код работает правильно:

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

print(f.__name__)  # prints 'f'
print(f.__doc__)   # prints 'does some math'
person Eli Courtwright    schedule 21.11.2008
comment
Существует решение проблемы справки, в котором используется модуль декоратора, и оно упоминается в комментариях к этому ответу: stackoverflow.com/questions/1782843/ - person Scott Griffiths; 25.11.2009
comment
Да, я предпочитаю избегать модуля декоратора, поскольку functools.wraps является частью стандартной библиотеки и, таким образом, не вводит другую внешнюю зависимость. Но модуль декоратора действительно решает проблему справки, которая, надеюсь, когда-нибудь также решит functools.wraps. - person Eli Courtwright; 25.11.2009
comment
вот пример того, что может случиться, если вы не используете обертки: тесты doctools могут внезапно исчезнуть. это потому, что doctools не могут найти тесты в декорированных функциях, если что-то вроде wraps () не скопировало их. - person andrew cooke; 25.04.2011
comment
зачем нам functools.wraps для этой работы, разве это не должно быть просто частью шаблона декоратора? когда бы вы не хотели использовать @wraps? - person wim; 02.01.2014
comment
@wim: Я написал несколько декораторов, которые выполняют свою собственную версию @wraps, чтобы выполнять различные типы модификации или аннотации к скопированным значениям. По сути, это расширение философии Python, согласно которой явное лучше, чем неявное, а особые случаи недостаточно особенные, чтобы нарушать правила. (Код намного проще, а язык легче понять, если @wraps необходимо вводить вручную, а не использовать какой-то специальный механизм отказа.) - person ssokolow; 30.03.2014
comment
@ssokolow: special cases aren't special enough to break the rules: если wraps было поведением по умолчанию, это будет правилом. wraps для меня больше похоже на патч, чем на более явный способ решения задач. Тем не менее, лучше вместо изменения существующих правил добавить патч и создать Python 4 с партером из 5 пользователей. - person Marco Sulla; 22.07.2015
comment
@LucasMalor Не все декораторы оборачивают функции, которые они украшают. Некоторые применяют побочные эффекты, например, регистрируют их в какой-то системе поиска. - person ssokolow; 23.07.2015
comment
но зачем нам восстанавливать имя функции, строку документации и т. д. и похоже, что @wraps - это патч к шаблону декоратора. в чем опасность, что мы теряем? какая функциональность не может быть предоставлена, если нет правильного имени функции, строки документации и т. д.? не могли ли это быть встроенными даже позже, когда эта проблема была обнаружена? какие проблемы обратной совместимости не удалось реализовать в самом языке? - person Rajendra Uppal; 21.08.2017
comment
Как компилятор вообще узнает, что это декоратор, чтобы знать, когда применять @wraps автоматически? - person McKay; 21.12.2017
comment
Похоже, декоратор - потенциально распространенный способ реализации части функциональной карты Functor (en.wikipedia. org / wiki / Functor) на Python, хотя я полагаю, что сама по себе эта идея не очень полезна с категориальной точки зрения. - person bbarker; 08.05.2018

Я очень часто использую в качестве декораторов классы, а не функции. У меня были некоторые проблемы с этим, потому что объект не будет иметь всех тех атрибутов, которые ожидаются от функции. Например, у объекта не будет атрибута __name__. У меня была конкретная проблема с этим, которую было довольно сложно отследить, где Django сообщал об ошибке «объект не имеет атрибута '__name__'». К сожалению, для декораторов в стиле классов я не верю, что @wrap справится со своей задачей. Вместо этого я создал базовый класс декоратора следующим образом:

class DecBase(object):
    func = None

    def __init__(self, func):
        self.__func = func

    def __getattribute__(self, name):
        if name == "func":
            return super(DecBase, self).__getattribute__(name)

        return self.func.__getattribute__(name)

    def __setattr__(self, name, value):
        if name == "func":
            return super(DecBase, self).__setattr__(name, value)

        return self.func.__setattr__(name, value)

Этот класс передает все вызовы атрибутов декорируемой функции. Итак, теперь вы можете создать простой декоратор, который проверяет, указаны ли два аргумента следующим образом:

class process_login(DecBase):
    def __call__(self, *args):
        if len(args) != 2:
            raise Exception("You can only specify two arguments")

        return self.func(*args)
person Josh    schedule 03.12.2009
comment
Как сказано в документации от @wraps, @wraps - это просто вспомогательная функция для functools.update_wrapper(). В случае декоратора класса вы можете вызвать update_wrapper() прямо из вашего __init__() метода. Итак, вам вообще не нужно создавать DecBase, вы можете просто включить __init__() из process_login строку: update_wrapper(self, func). Это все. - person Fabiano; 14.02.2019
comment
Просто чтобы другие тоже нашли этот ответ: Flask с его add_url_route требует (в некоторых случаях?), Чтобы предоставленная функция view_func имела __name__, что уже не так, если предоставленная функция на самом деле является декорированным методом, даже когда functools.wraps используется в декораторе. - person Joël; 04.10.2020
comment
И в результате +1 для @Fabiano: использование update_wrapper вместо @wraps делает свою работу :) - person Joël; 04.10.2020

Начиная с Python 3.5+:

@functools.wraps(f)
def g():
    pass

Псевдоним для g = functools.update_wrapper(g, f). Он делает ровно три вещи:

Следствием этого является то, что g имеет то же имя, строку документации, имя модуля и подпись, что и f. Единственная проблема в том, что в отношении подписи это не совсем так: просто inspect.signature по умолчанию следует цепочкам оберток. Вы можете проверить это, используя inspect.signature(g, follow_wrapped=False), как описано в документе. Это имеет неприятные последствия:

  • код оболочки будет выполняться, даже если предоставленные аргументы недействительны.
  • код оболочки не может легко получить доступ к аргументу, используя его имя из полученных * args, ** kwargs. Действительно, нужно было бы обрабатывать все случаи (позиционные, ключевые слова, по умолчанию) и, следовательно, использовать что-то вроде Signature.bind().

Теперь есть небольшая путаница между functools.wraps и декораторами, потому что очень частый вариант использования при разработке декораторов - это обертывание функций. Но оба являются полностью независимыми концепциями. Если вам интересно понять разницу, я реализовал вспомогательные библиотеки для обоих: decopatch для записи декораторы, а также makefun, чтобы обеспечить замену @wraps с сохранением подписи. Обратите внимание, что makefun использует тот же проверенный прием, что и знаменитая библиотека decorator.

person smarie    schedule 11.03.2019

  1. Предпосылка: вы должны знать, как использовать декораторы, особенно с обертками. Этот комментарий объясняет это немного ясно или это ссылка также довольно хорошо это объясняет.

  2. Каждый раз, когда мы используем For, например: @wraps, за которым следует наша собственная функция-оболочка. Согласно сведениям, приведенным в этой ссылке, говорится, что

functools.wraps - это удобная функция для вызова update_wrapper () в качестве декоратора функции при определении функции-оболочки.

Это эквивалентно частичному (update_wrapper, wrapped = wrapped, назначено = назначено, обновлено = обновлено).

Итак, декоратор @wraps фактически вызывает функцию functools.partial (func [, * args] [, ** keywords]).

Определение functools.partial () говорит, что

Частичный () используется для частичного приложения функции, которое «замораживает» некоторую часть аргументов функции и / или ключевых слов, в результате чего создается новый объект с упрощенной подписью. Например, partial () можно использовать для создания вызываемого объекта, который ведет себя как функция int (), где базовый аргумент по умолчанию равен двум:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

Это подводит меня к выводу, что @wraps вызывает частичную () и передает ей вашу функцию-оболочку в качестве параметра. В конце partial () возвращает упрощенную версию, то есть объект того, что находится внутри функции-оболочки, а не саму функцию-оболочку.

person 3rdi    schedule 10.03.2018

это исходный код оберток:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')

WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):

    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

   Returns a decorator that invokes update_wrapper() with the decorated
   function as the wrapper argument and the arguments to wraps() as the
   remaining arguments. Default arguments are as for update_wrapper().
   This is a convenience function to simplify applying partial() to
   update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)
person Baliang    schedule 14.12.2018

Короче говоря, functools.wraps - это обычная функция. Давайте рассмотрим этот официальный пример. С помощью исходного кода мы можем см. более подробную информацию о реализации и выполняемых этапах следующим образом:

  1. wraps (f) возвращает объект, например O1. Это объект класса Partial
  2. Следующий шаг - @ O1 ..., который является обозначением декоратора в Python. Это означает

wrapper = O1 .__ call __ (оболочка)

Проверка реализации __call__ a>, мы видим, что после этого шага (левая сторона) оболочка становится объектом, полученным с помощью self.func (* self.args, * args, ** newkeywords) Проверяя создание O1 в __new__, мы знаем, что self.func - это функция update_wrapper. Он использует параметр * args, находящийся в правой части оболочки, в качестве 1-го параметра. Проверив последний шаг update_wrapper, можно увидеть, что возвращается оболочка с правой стороны, при этом некоторые атрибуты изменяются по мере необходимости.

person Yong Yang    schedule 30.03.2018