Что делает functools.wraps? - PullRequest
523 голосов
/ 21 ноября 2008

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

Ответы [ 6 ]

895 голосов
/ 21 ноября 2008

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

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'
20 голосов
/ 04 декабря 2009

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

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

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

        return self.func(*args)
3 голосов
/ 11 марта 2019

Начиная с 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 source .
  • обновляет __dict__ из g со всеми элементами из f.__dict__. (см. WRAPPER_UPDATES в источнике)
  • устанавливает новый атрибут __wrapped__=f на g

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

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

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

3 голосов
/ 14 декабря 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)
1 голос
/ 10 марта 2018
  1. Необходимое условие: Вы должны знать, как использовать декораторы, особенно с обертками. Этот комментарий объясняет это немного ясно, или эта ссылка также объясняет это довольно хорошо.

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

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

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

Итак, @wraps decorator фактически вызывает functools.partial (func [, * args] [, ** ключевые слова]).

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

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

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

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

0 голосов
/ 30 марта 2018

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

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

обертка = O1 .__ вызов __ (обертка)

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

...