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

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

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
724
0
158 935
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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'

Существует решение проблемы справки, в котором используется модуль декоратора, и оно упоминается в комментариях к этому ответу: stackoverflow.com/questions/1782843/…

Scott Griffiths 25.11.2009 18:16

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

Eli Courtwright 25.11.2009 19:37

вот пример того, что может случиться, если вы не используете обертки: тесты doctools могут внезапно исчезнуть. это потому, что doctools не может найти тесты в декорированных функциях, если что-то вроде wraps () не скопировало их.

andrew cooke 25.04.2011 02:00

Зачем нам нужен functools.wraps для этой работы, разве он не должен быть частью шаблона декоратора? когда бы вы нет захотели использовать @wraps?

wim 02.01.2014 19:46

@wim: Я написал несколько декораторов, которые создают свою собственную версию @wraps, чтобы выполнять различные типы модификации или аннотации скопированных значений. По сути, это расширение философии Python, согласно которой явное лучше, чем неявное, а особые случаи недостаточно особенные, чтобы нарушать правила. (Код намного проще, а язык легче понять, если @wraps должен быть предоставлен вручную, а не с использованием какого-то специального механизма отказа.)

ssokolow 30.03.2014 03:58

@ssokolow: special cases aren't special enough to break the rules: если wraps был поведением по умолчанию, это будет «правилом». wraps для меня больше похож на патч, чем на более явный способ решения задач. Тем не менее, лучше вместо изменения существующих правил добавить патч и создать Python 4 с партером из 5 пользователей.

Marco Sulla 22.07.2015 20:27

@LucasMalor Не все декораторы оборачивают функции, которые они украшают. Некоторые применяют побочные эффекты, например, регистрируют их в какой-то системе поиска.

ssokolow 23.07.2015 03:46

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

Rajendra Uppal 21.08.2017 20:26

Как компилятор вообще узнает, что это декоратор, чтобы знать, когда применять @wraps автоматически?

McKay 21.12.2017 08:33

Похоже, декоратор - потенциально распространенный способ реализовать часть функциональной карты Functor (en.wikipedia.org/wiki/Functor) в Python, хотя я думаю, что сама по себе эта идея не очень полезна с категориальной точки зрения.

bbarker 08.05.2018 18:25

Я очень часто использую в качестве декораторов классы, а не функции. У меня были некоторые проблемы с этим, потому что объект не будет иметь всех тех атрибутов, которые ожидаются от функции. Например, у объекта не будет атрибута __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)

Как говорится в документации от @wraps, @wraps - это просто вспомогательная функция для functools.update_wrapper(). В случае декоратора классов вы можете вызвать update_wrapper() прямо из вашего метода __init__(). Итак, вам вообще не нужно создавать DecBase, вы можете просто включить на __init__() из process_login строку: update_wrapper(self, func). Это все.

Fabiano 14.02.2019 00:34

Просто чтобы другие тоже нашли этот ответ: Flask с его add_url_route требует (в некоторых случаях?), Чтобы предоставленная функция view_func имела __name__, что уже не так, если предоставленная функция на самом деле является декорированным методом, даже когда functools.wraps используется в декораторе.

Joël 04.10.2020 13:03

И в результате +1 для @Fabiano: использование update_wrapper вместо @wraps делает работу :)

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

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

functools.wraps is convenience function for invoking update_wrapper() as a function decorator, when defining a wrapper function.

It is equivalent to partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated).

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

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

The partial() is used for partial function application which “freezes” some portion of a function’s arguments and/or keywords resulting in a new object with a simplified signature. For example, partial() can be used to create a callable that behaves like the int() function where the base argument defaults to two:

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

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

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

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

wrapper=O1.__call__(wrapper)

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

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

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)

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

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

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

  • он копирует атрибуты __module__, __name__, __qualname__, __doc__ и __annotations__f на g. Этот список по умолчанию находится в WRAPPER_ASSIGNMENTS, вы можете увидеть его в источник functools.
  • он обновляет __dict__g всеми элементами из f.__dict__. (см. WRAPPER_UPDATES в источнике)
  • он устанавливает новый атрибут __wrapped__=f на g

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

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

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

Другие вопросы по теме