Почему декораторы выполняются в разном порядке?

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

'''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 шт):

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

Это иллюзия!

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) в порядке СВЕРХУ ВНИЗ
→ Ссылка
Автор решения: CrazyElf

Просто добавьте ещё функций печати и всё станет понятно:

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

Ну то есть задумка именно такая, что декораторы должны выполняться в прямом порядке, так, как они написаны. А для этого код, создающий декораторы, должен выполняться в обратном порядке, что и сделано в Питоне.

→ Ссылка
Автор решения: Stepan Roze

Вы подняли интересный вопрос о применении декораторов в 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))

В этом примере декораторы применяются следующим образом:

  1. Сначала применяется italic к функции greet, и мы получаем функцию, которая оборачивает результат greet в <i>.
  2. Затем применяется 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'))

В этом примере порядок применения декораторов такой же:

  1. Сначала применяется dec2 к функции function, и мы получаем функцию, которая печатает "Второй декоратор" перед вызовом function.
  2. Затем применяется dec1 к результату предыдущего шага, оборачивая вызов в дополнительный принт "Первый декоратор".

Таким образом, результат:

  • Вызывается сначала dec1, который печатает "Первый декоратор".
  • Затем внутри него вызывается dec2, который печатает "Второй декоратор".
  • Затем вызывается исходная функция function.

Результат:

Первый декоратор
Второй декоратор
fdsfsdf

Вывод

Оба примера следуют одному и тому же правилу применения декораторов: "снизу вверх". Разница в восприятии возникает из-за того, что в первом примере декораторы модифицируют возвращаемое значение функции, тогда как во втором примере они добавляют побочный эффект (печать в консоль) до вызова функции. Таким образом, в обоих случаях декораторы работают корректно и применяются в одном и том же порядке, просто их влияние на итоговый результат различается.

→ Ссылка