Що робить functools.wraps?


650

У коментарі до цієї відповіді на інше питання хтось сказав, що вони не впевнені, що functools.wrapsце робить. Отже, я задаю це запитання, щоб він був записом його в StackOverflow для подальшої довідки: що functools.wrapsробити саме?

Відповіді:


1069

Використовуючи декоратор, ви замінюєте одну функцію іншою. Іншими словами, якщо у вас є декоратор

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тому що це назва вашої нової функції. Насправді, якщо ви подивитеся на docstring для f, він буде порожнім, тому що with_loggingне має docstring, і тому docstring, який ви написали, більше не буде. Крім того, якщо ви подивитеся на результат pydoc для цієї функції, він не буде вказаний як один аргумент x; замість цього він буде вказаний як взяття, *argsі **kwargsтому, що для цього потрібно.

Якщо використання декоратора завжди означало втрату цієї інформації про функцію, це було б серйозною проблемою. Тому ми маємоfunctools.wraps . Це займає функцію, яка використовується в декораторі, і додає функціональність копіювання через ім'я функції, docstring, список аргументів і т. Д. І оскільки 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'

7
Так, я вважаю за краще уникати модуля декораторів, оскільки functools.wraps є частиною стандартної бібліотеки і, таким чином, не вводить іншої зовнішньої залежності. Але модуль декораторів справді вирішує проблему допомоги, яка, сподіваємось, також функціонує.
Eli Courtwright

6
ось приклад того, що може статися, якщо ви не використовуєте обгортання: тести на doctools можуть раптово зникнути. це тому, що doctools не можуть знайти тести в оформлених функціях, якщо щось на зразок wraps () не скопіювало їх поперек.
Андре Кук

88
для чого нам потрібна functools.wrapsця робота, чи не повинна вона в першу чергу бути лише частиною декоративного малюнка? коли ви не хочете використовувати @wraps?
Вім

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

35
@LucasMalor Не всі декоратори виконують функції, які вони прикрашають. Деякі застосовують побічні ефекти, такі як реєстрація їх у якійсь системі пошуку.
ssokolow

22

Я дуже часто використовую заняття, а не функції, для своїх декораторів. У мене були проблеми з цим, оскільки об’єкт не буде мати однакових атрибутів, які очікуються від функції. Наприклад, об’єкт не буде мати атрибут __name__. У мене була проблема з цим, що було досить важко простежити, де Джанго повідомляв про помилку "об’єкт не має атрибута __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)

7
Як @wrapsговориться в документах , @wrapsце просто зручність функції functools.update_wrapper(). У разі оформлення класу, ви можете зателефонувати update_wrapper()безпосередньо зі свого __init__()методу. Таким чином, вам не потрібно створювати DecBaseна всіх, ви можете просто включити на __init__()з process_loginлінії: update_wrapper(self, func). Це все.
Фабіано

14

Станом на python 3.5+:

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

Псевдонім для g = functools.update_wrapper(g, f). Це робить саме три речі:

  • він копіює __module__, __name__, __qualname__, __doc__, і __annotations__атрибути fна g. Цей список за замовчуванням є WRAPPER_ASSIGNMENTS, ви можете побачити його у джерелі функцій .
  • він оновлює __dict__з gусіх елементів f.__dict__. (див. WRAPPER_UPDATESу джерелі)
  • він встановлює новий __wrapped__=fатрибутg

Наслідком цього є те, що gсхоже на те, що має те саме ім'я, docstring, ім'я модуля та підпис, ніж f. Єдина проблема полягає в тому, що стосовно підпису це насправді не так: саме inspect.signatureза замовчуванням йде ланцюжок обгортки. Ви можете перевірити це, використовуючи inspect.signature(g, follow_wrapped=False)пояснення в документі . Це має дратівливі наслідки:

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

Зараз існує певна плутанина між functools.wrapsдекораторами, адже дуже частою справою для розробки декораторів є використання функцій. Але обидва є абсолютно самостійними поняттями. Якщо вам цікаво зрозуміти різницю, я застосував бібліотеки-помічники для обох: decopatch, щоб легко писати декоратори, і makefun, щоб забезпечити заміну, що зберігає підписи @wraps. Зауважте, що makefunпокладається на той самий перевірений трюк, що й відома decoratorбібліотека.


3

це вихідний код про обгортання:

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)

2
  1. Необхідна умова: Ви повинні знати, як користуватися декораторами та спеціально з обгортаннями. Цей коментар пояснює це трохи чітко, або це посилання також досить добре пояснює його.

  2. Щоразу, коли ми використовуємо для Наприклад, наприклад: @wraps, за яким слідує наша функція обгортки. Відповідно до деталей, наведених у цьому посиланні , воно говорить про це

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

Вона еквівалентна частковій (update_wrapper, wrapped = загорнута, призначена = призначена, оновлена ​​= оновлена).

Тож декоратор @wraps насправді дзвонить на functools.partial (func [, * args] [, ** ключові слова]).

Визначення functools.partial () говорить про це

Частинка () використовується для часткового застосування функції, яка "заморожує" деяку частину аргументів функції та / або ключові слова, в результаті чого новий об'єкт має спрощену підпис. Наприклад, parcial () може бути використаний для створення дзвінка, який веде себе як функція int (), де базовий аргумент за замовчуванням до двох:

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

Що підводить мене до висновку, що @wraps викликає частковий (), і він передає вашу функцію обгортки як параметр йому. Часткове () в кінцевому підсумку повертає спрощену версію, тобто об'єкт того, що знаходиться всередині функції обгортки, а не саму функцію обгортки.


-4

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

  1. wraps (f) повертає об'єкт, скажімо, O1 . Це об'єкт класу Partial
  2. Наступний крок - @ O1 ..., який є позначенням декоратора в python. Це означає

обгортка = O1 .__ виклик __ (обгортка)

Перевіряючи реалізацію __call__ , ми бачимо, що після цього кроку (ліва сторона) обгортка стає об'єктом в результаті self.func (* self.args, * args, ** newkeywords) Перевіряючи створення O1 у __new__ , ми Знайте self.func - це функція update_wrapper . В якості першого параметра він використовує параметр * args , праворучну обгортку . Перевіряючи останній крок update_wrapper , видно, що повернута права оболонка повернута, з деякими атрибутами змінено за потребою.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.