Как работает декоратор внутри?

У нас есть простой декоратор и функция, которой мы хотим добавить дополнительный функционал:

def wrap(func):
    def mod():
        up = func()
        mod_up = up.upper()
        return mod_up
    return mod

def up_str():
    return 'hello Maksim'

Задекорировать и вызвать нашу исходную функцию мы можем тремя способами :

#1) Просто вызывать функцию wrap c передачей ей в качестве параметра функции uper_str.
print(wrap(up_str)()) #Вывод: HELLO MAKSIM Важным моментом является то, что мы всегда возвращаем в return не результат вызова 
#функции(с круглыми скобочками), а сам объект функции(без круглых скобочек).

# Этап вызова нашей функции ^^^ когдa выполняется return mod, объект функции mod перемещается в область видимости wrap, что образно можно представить так:
def wrap(func):
    mod
# А затем к возвращённому mod применяется оператор круглых скобочек (), которые мы дописали в конце вызова wrap выше. То есть вызывается функция mod() - происходит то же самое, что и в предыдущий раз тело функции mod как бы перемещается в область видимости wrap:
def wrap(func):
    up = func()
    mod_up = up.upper() # эти две строчки сразу возвращаются в виде результата вызова mod() - return mod_up. Это просто образное представление происходящих процессов, чтобы в том числе объяснить, как параметр func может передаться в up = func() - благодаря попадaнию в область видимости wrap.
# (Если что я просто образно представляю порядок выполнения кода)

#2) Переопределить функцию up_str. Логика "превращений" та же только добавляется новое звено: wrap(up_str)-->mod-->up_str. Чтобы активировать переопределённую функцию к ней так же надо добавить оператор круглых скобок () :
up_str = wrap(up_str) 
print(up_str()) #Вывод: HELLO MAKSIM
# В этом случае после переопределения функции up_str мы теряем её исходный функционал, но мы всегда можем сохранить исходную функцию в другую переменную до её переопределения и таким образом ничего не потерять.

#3) Ну и с помощью синтаксического сахара:
@wrap
def up_str():
    return 'hello Maksim'

print(up_str())#Вывод: HELLO MAKSIM

В первых двух способах функция декорируется на этапе вызова функции. В последнем, на этапе определения функции. Отсюда следуют вопросы:

  1. Что последовательно происходит при декорировании через @, на этапе определения up_str?
  2. Как сохранить и исходную функцию, и задекорированную при использовании @?

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

Автор решения: vitidev

1 При подключении модуля код модуля выполняется. Питон выполняет код модуля "сверху вниз", тем самым заполняя пространство имен. Когда при этом выполнении он встречает декоратор @decoratorname (который по существу Callable), то он вызывает его как callable (на этапе определения декодируемого объекта, когда он уже распарсил декорируемый объект, но еще не впихнул его в пространство имен), передавая ему декорируемый объект (ведь всё есть объект в питоне и определяемая функция тоже).

Далее эта функция-декоратор должна что то вернуть - и это попадет в пространство имен под именем декорируемого объекта. Эквивалентно вашему up_str = wrap(up_str). Но можно хоть 42 вернуть.

В данном примере вместо функции up_str под именем up_str в пространстве имен будет число 42.

def wrap(func):
    return 42

@wrap
def up_str():
    return 'hello Maksim'

print(type(up_str))

# <class 'int'>

2 не будет никакой исходной функции в итоге. в месте вызова декоратора будет всегда результат выполнения декоратора. Что бы это ни было. Вот в нем вы можете сохранять что угодно и как угодно. Обычно возвращают обертку на функцией, а внутри обертки хранится оригинальная функция.

ps: существует functools.wraps который делает всю грязную работу при создании обертке, а это не только само оборачивание.

→ Ссылка