Обьясните как работает @wraps(func) детально пжл

Знаю что @wraps(func) снимает something() из inner функции но подскажите принцип работы пжл шаг за шагом

from timeit import default_timer as timer
import math 
import time
from functools import wraps

def decorator(func):
    @wraps(func)
    def inner(*args):
        start = timer()
        print(f"Function {func.__name__} starts its work")
        func(*args)
        end = timer()
        print(f"Function {func.__name__} took {end-start} time")

    return inner


@decorator
def something(a,b):
    time.sleep(3)
    print(a+b)
something(4,5)
#print(help(something))

Ответы (1 шт):

Автор решения: OPTIMUS PRIME

Любой декоратор вида

def decor(): ...

@decor
def func(): ...

можно переписать так:

def decor(): ...

def func(): ...

func = decor(func)

В вашем случае,

@wraps(func)
def inner(): ...

То же, что и

def inner(): ...

inner = wraps(func)(inner)
# вызов wraps(func) возвращает другую функцию,
# которая сама выступает в роли декоратора, сразу же вызывается с аргументом (inner)

wraps пытается сделать обертку inner максимально похожей на исходную func

  1. Берет значения атрибутов ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__') у функции func и присваивает их функции inner

  2. Все значения из func.__dict__ переносит в inner.__dict__. (Справка: __dict__ — словарь, хранящий все атрибуты объекта. В случае любого присваивания атрибута а-ля func.x = 0, он сохраняется в словаре func.__dict__['x'])

  3. Сохраняет ссылку на исходную функцию inner.__wrapped__ = func


Но просили пошагово - будет пошагово xD

Возможно, для начала стоит переписать @декораторы в виде обычного присваивания, для наглядности. Этот код делает то же самое, что и ваш:

#1

from timeit import default_timer as timer
import time
from functools import wraps

def decorator(func):
    # @wraps(func)
    def inner(*args):
        start = timer()
        print(f"Function {func.__name__} starts its work")
        func(*args)
        end = timer()
        print(f"Function {func.__name__} took {end-start} time")

    wrap = wraps(func)     # (*1)
    inner = wrap(inner)    # partial.__call__(wrap, inner)
                           # Пока не обращайте внимания.

    print(wrap.__class__)  # (!) <class 'functools.partial'>

    return inner

# @decorator
def something(a, b):
    time.sleep(3)
    print(a + b)

something = decorator(something) # ◄

something(4, 5)

Полез в исходный код functools (во всех кодах ниже, часть проверок и комментариев вырезаны), который и у вас должен лежать где-то тут:

C:\Users\USER\AppData\Local\Programs\Python\Python39\Lib\functools.py

Раз уж сперва вызывается wraps(func), посмотрим, что он из себя представляет:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):

    """Decorator factory to apply update_wrapper() to a wrapper function"""

    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

Т.е. у нас функция func, он же, something идет сюда в виде параметра wrapped

update_wrapper — какая-то вспомогательная функция, передается в виде аргумента.
Часть кода partial:

class partial:
    """New function with partial application of the given arguments and keywords"""

    __slots__ = "func", "args", "keywords", "__dict__", "__weakref__"

    def __new__(cls, func, /, *args, **keywords):
        self = super(partial, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords
        return self

    def __call__(self, /, *args, **keywords):
        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)

Справка: object.__new__

При вызове partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated) сперва вызывается его __new__. Где cls - это сам partial, а func - функция update_wrapper. Тут не происходит ничего особенного, всего лишь сохраняет ссылку на update_wrapper в атрибуте .func, а wrapped, assigned, updated - в виде словаря .keywords. А self.args в данном случае остается пустым.


На минуточку вернемся к блоку кода #1, в строчке (*1) если wrap — это экземпляр класса partial, получается, при вызове inner = wrap(inner) будет вызван метод __call__ (он вызывается всякий раз, когда экземпляр класса пытаются вызвать как функцию)

partial.__call__(wrap, inner) // wrap → self, inner → попадает в *args

__call__ в свою очередь вызывает self.func (это update_wrapper).
Получается вызов update_wrapper(*self.args, *args, **keywords) где:
*self.args пустой
*args содержит одну функцию inner
keywords - ранее сохраненные wrapped, assigned, updated

update_wrapper(inner, wrapped=wrapped, assigned=assigned, updated=updated) — в итоге имеем такой вызов, где wrapped - ссылка на исходную функцию something


А что с update_wrapper ?

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

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

    for attr in assigned:
        # assigned = ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)

    for attr in updated:
        # updated = ('__dict__',)
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))

    wrapper.__wrapped__ = wrapped
    return wrapper

Берет все необходимые атрибуты у something и присваивает их функции inner, сохраняет ссылку на исходную функцию в атрибуте __wrapped__

→ Ссылка