Почему декораторы выполняются в разном порядке?
Разбираюсь с декораторами. Как гласит теория, применяются они снизу вверх, тоесть кто ближе к описанию декорируемой функции, тот и первый. Однако код
'''case 1'''
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return '<b>' + func(*args, **kwargs) + '</b>'
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return '<i>' + func(*args, **kwargs) + '</i>'
return wrapper
strng = '1123123123'
@bold
@italic
def greet(string):
return string
print(greet(strng))
"""Case 2:"""
def dec1(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Первый декоратор')
value = func(*args, **kwargs)
return value
return wrapper
def dec2(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Второй декоратор')
value = func(*args, **kwargs)
return value
return wrapper
@dec1
@dec2
def function(string):
return string
print(function('fdsfsdf'))
Выдаёт мне результат
<b><i>Hello world!</i></b>
Первый декоратор
Второй декоратор
fdsfsdf
Вопрос - почему в первом случае применяются декораторы как в теории, сначала italic затем bold, снизу вверх. А во втором случае сначала dec1, который сверху, а после него dec2, что снизу? Не вижу, чем отличаются описания декораторов и функций, в чем причина разного поведения?
Ответы (3 шт):
Это иллюзия!
Case 3:
import functools
def dec1(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Первый декоратор')
return 'Первый ' + func(*args, **kwargs) + ' Первый'
return wrapper
def dec2(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Второй декоратор')
return 'Второй ' + func(*args, **kwargs) + ' Второй'
return wrapper
@dec1
@dec2
def function(string):
return string
print(function('DFFFFFFFFD'))
Вывод:
Первый декоратор
Второй декоратор
Первый Второй DFFFFFFFFD Второй Первый
Т.е.:
- аргумент переданный в
function('DFFFFFFFFD')
оборачивается СНИЗУ ВВЕРХ, - код который выполняется в
wrapper(*args, **kwargs)
в порядке СВЕРХУ ВНИЗ
Просто добавьте ещё функций печати и всё станет понятно:
import functools
def bold(func):
print('Декоратор bold')
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('Вызов bold')
return '<b>' + func(*args, **kwargs) + '</b>'
return wrapper
def italic(func):
print('Декоратор italic')
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('Вызов italic')
return '<i>' + func(*args, **kwargs) + '</i>'
return wrapper
strng = '1123123123'
@bold
@italic
def greet(string):
print('Вызов greet')
return string
print(greet(strng))
Вывод:
Декоратор italic
Декоратор bold
Вызов bold
Вызов italic
Вызов greet
<b><i>1123123123</i></b>
Код, который создаёт декораторы, действительно вызывается в обратном порядке. Декораторы оборачивают друг друга. В самой глубине оказывается декорируемая функция, последний декоратор оборачивает эту функцию, предпоследний декоратор оборачивает этот последний декоратор и т.д.
А вот когда эта вся матрёшка (спасибо Stanislav Volodarskiy за термин) выполняется, то выполняется она уже сверху вниз, потому что снаружи находится именно самый первый декоратор, который последним всё это оборачивал. Слои раскрываются в порядке обратном тому, как они наматывались. Первый декоратор вызывает второй декоратор и т.д., последний декоратор вызывает декорированную функцию.
Ну то есть задумка именно такая, что декораторы должны выполняться в прямом порядке, так, как они написаны. А для этого код, создающий декораторы, должен выполняться в обратном порядке, что и сделано в Питоне.
Вы подняли интересный вопрос о применении декораторов в Python, и ваше наблюдение о различии в поведении двух примеров действительно требует объяснения. Применение декораторов всегда происходит "снизу вверх", то есть декоратор, который находится ближе к функции, применяется первым.
Рассмотрим ваши два примера более детально:
Пример 1:
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return '<b>' + func(*args, **kwargs) + '</b>'
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return '<i>' + func(*args, **kwargs) + '</i>'
return wrapper
strng = '1123123123'
@bold
@italic
def greet(string):
return string
print(greet(strng))
В этом примере декораторы применяются следующим образом:
- Сначала применяется
italic
к функцииgreet
, и мы получаем функцию, которая оборачивает результатgreet
в<i>
. - Затем применяется
bold
к результату предыдущего шага, оборачивая результат в<b>
.
Таким образом, результат: '<b><i>1123123123</i></b>'
.
Пример 2:
import functools
def dec1(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Первый декоратор')
value = func(*args, **kwargs)
return value
return wrapper
def dec2(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Второй декоратор')
value = func(*args, **kwargs)
return value
return wrapper
@dec1
@dec2
def function(string):
return string
print(function('fdsfsdf'))
В этом примере порядок применения декораторов такой же:
- Сначала применяется
dec2
к функцииfunction
, и мы получаем функцию, которая печатает "Второй декоратор" перед вызовомfunction
. - Затем применяется
dec1
к результату предыдущего шага, оборачивая вызов в дополнительный принт "Первый декоратор".
Таким образом, результат:
- Вызывается сначала
dec1
, который печатает "Первый декоратор". - Затем внутри него вызывается
dec2
, который печатает "Второй декоратор". - Затем вызывается исходная функция
function
.
Результат:
Первый декоратор
Второй декоратор
fdsfsdf
Вывод
Оба примера следуют одному и тому же правилу применения декораторов: "снизу вверх". Разница в восприятии возникает из-за того, что в первом примере декораторы модифицируют возвращаемое значение функции, тогда как во втором примере они добавляют побочный эффект (печать в консоль) до вызова функции. Таким образом, в обоих случаях декораторы работают корректно и применяются в одном и том же порядке, просто их влияние на итоговый результат различается.