С Python я знаком давно, в основном пишу бэкенд на Django. Сейчас работаю на нескольких работах, на одной выполняю роль бэкенд‑разработчика, а на другой — лида веб отдела.

Недавно наткнулся на тему в вузе, которую я знал всегда, использовал постоянно, но никогда не изучал никакой теории - Декораторы. Используются они много где, особенно удобно в фреймворках просто перед функцией написать какую‑нибудь магическую строчку с @ и всё готово. Понимал как они работают, но учиться никогда не поздно, так что попробую разобрать основные технические детали работы декораторов (только для функций).

Статья подойдёт тем, кто просто зашёл узнать пару фактов про декораторы и хочет узнать синтаксис. Также раскрою немного глубже в техническую часть декораторов. Хорошие источники по этой теме — в конце. Также была использована великолепная статья:

«Python декораторы на максималках. Универсальный рецепт по написанию и аннотированию от мала до велика»


В книге Марка Лутца «Изучаем Python» декораторам посвящена целая глава. И не удивительно, ведь они являются большой частью написания функций. Начинается глава с определения декоратора:

«Декорирование представляет собой способ указания управляющего или дополняющего кода для функций и классов. Сами декораторы принимают вид вызываемых объектов (например, функций), обрабатывающих другие вызываемые объекты. Как было показано ранее в книге, декораторы Python имеют две связанные друг с другом формы, ни одна из которых не требует Python З.Х или классов нового стиля.

  • Декораторы функций, добавленные в Python 2.4, делают повторное привязывание имен во время определения функций, предоставляя уровень логики, который может управлять функциями и методами или последующими обращениями к ним.

  • Декораторы классов, добавленные в Python 2.6 и 3.0, делают повторное привязывание имен во время определения классов, предоставляя уровень логики, который может управлять классами или экземплярами, созданными при последующих обращениях к классам.»

Честно – не особо понятно ?

Посмотрим, что говорит Дэн Бейнер в книге «Чистый Python. Тонкости программирования для профи»:

«Итак, что же такое декораторы на самом деле? Они «украшают», или «обертывают», другую функцию и позволяют исполнять программный код до и после того, как обернутая функция выполнится. Декораторы позволяют определять конструктивные блоки многократного использования, которые могут изменять или расширять поведение других функций. И они позволяют это делать без необратимых изменений самой обернутой функции. Поведение функции изменяется, только когда оно декорировано.»

Так-то лучше. Декоратор – обертка функции, но как оно работает? Пример из всё той же книги про чистый python:

def null_decorator(func):
    return func  

def greet():     
    return 'Привет!'  

greet = null_decorator(greet)  

>>> greet()  
'Привет!'

Поздравляю, вы увидели самый простой декоратор, который сможете увидеть. Но там было что-то с @, а где оно? Оказывается, без лишних заморочек можно просто сделать так:

@null_decorator 
def greet(): 
    return 'Привет!' 

>>> greet()
'Привет!'

В данном примере функция greet сначала определяется, а затем прогоняется через наш декоратор null_decorator. Синтаксис @ делает то же самое, что мы раньше делали, когда вызывали

greet = null_decorator(greet)

Только с @ мы больше не сможем вызвать недекорированную функцию. Но если нам таковая никогда и не понадобиться, смело используем @.

До этого мы написали декоратор для нульарной (не принимающей аргументы) функции, а если функция принимает аргументы? Конечно, наш декоратор не заработает в таком случае. Давайте же посмотрим, как декорировать функцию, принимающую произвольные аргументы:

def proxy(func): 
    def wrapper(*args, **kwargs): 
        return func(*args, **kwargs) 
    return wrapper

«*args и **kwargs это функциональные средства языка Python для работы с неизвестными количествами аргументов.»

При написании декоратора мы, по сути, заменяем одну функцию другой. Проблема в том, что при этом «скрываются» некоторые метаданные, закрепленные за оригинальной (недекорированной) функцией.

Например, оригинальное имя функции, ее строка документации docstring и список параметров скрыты замыканием - оберткой:

def greet(): 
    """Вернуть дружеское приветствие.""" 
    return 'Привет!' 

decorated_greet = uppercase(greet) 

При попытке получить доступ к каким-либо из этих метаданных функции вместо них мы увидим метаданные замыкания - обертки:

>>> greet.__name__ 
'greet' 
>>> greet.__doc__ 
'Вернуть дружеское приветствие.' 

>>> decorated_greet.__name__ 
'wrapper' 
>>> decorated_greet.__doc__ 
None

И это делает отладку немного неудобной во многих случаях. К счастью, за 35 лет существования Python решение уже придумали. Заключается оно в простом использовании декоратора functools.wraps, который включен в стандартную библиотеку Python.

Декоратор functools.wraps можно использовать в своих собственных декораторах для того, чтобы копировать потерянные метаданные из недекорированной функции в замыкание декоратора. Вот пример:

import functools 

def uppercase(func): 
    @functools.wraps(func) 
    def wrapper(): 
        return func().upper() 
    return wrapper

Вот теперь все метаданные сохраняться.

Да, мы только что использовали декоратор в декораторе :3

Тогда использование нескольких декораторов для функции нас уже не должно удивить, верно? … Верно?....

@strong 
@emphasis 
def greet(): 
    return 'Привет!'

Не буду расписывать декораторы strong и emphasis, так как это не особо важно, важно то, в каком порядке они выполняются. А выполняются они не сверху вниз, а снизу вверх. То есть сначала объявляется функция, затем оборачивается в ближайший декоратор, затем в следующий и тд. Мне стало интересно, есть ли ограничение по количеству декораторов, и я такого не нашел (думаю это логично, ведь декоратор – не что-то магическое из другой вселенной).

Помимо всего этого в декоратор можно передавать значения также, как и в функцию. Что меня ввело в ступор, когда я начал разбираться, но всё довольно просто. Мы напишем декоратор, потом напишем функцию, которая уже будет принимать параметры и оборачивать декоратор. То есть у нас получится уже 3 уровня:

  1. Недекорированная функция

  2. Декоратор для неё

  3. Функция, принимающая аргументы и оборачивающая декоратор.

Сложно, да, рассмотрим на примере:

def decorator_wrapper(arg1, arg2):
    def real_decorator(func): # объявляем декоратор
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper

    return real_decorator # возвращаем декоратор


@decorator_wrapper(1, 2)
def func():
    ...

И так можно до бесконечности (понятное дело нет, но вложенность уровней можно и нужно добавлять при необходимости). Нужно это, к примеру, в случае если в одном декораторе передаются параметры, в другом нет, этакая перегрузка декоратора. В таком случае придется добавить 4-й уровень, который будет совмещать обычный декоратор и декоратор с параметрами. Такие декораторы называются декораторами Шредингера. Вот пример:

def schrodinger_decorator(
    call = None,
    *,
    arg1 = None,
    arg2 = None,
): ...

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

Затем мы отдельно пишем декоратор с параметрами еще раз:

def decorator_wrapper(arg1, arg2):
    def real_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    return real_decorator

И теперь останется только дописать логику если call is None:

def schrodinger_decorator(
        call=None,
        *,
        arg1=None,
        arg2=None,
):
    wrap_decorator = decorator_wrapper(arg1, arg2)

    # если мы использовали декоратор
    # как декоратор с параметрами
    if call is None:
        return wrap_decorator

    # если мы использовали декоратор как обычный
    ## arg1 и arg2 при этом принимают
    ## значения по умолчанию
    else:
        return wrap_decorator(Call)

Готово, можно использовать:

@schrodinger_decorator
def func(): ...

@schrodinger_decorator(arg1=1, arg2=2)
def func(): ...

У декораторов ещё очень много функционала, я выписал основные пункты из вышеуказанных книг:

  • Изменение и дополнение поведения функции

  • Управление и администрирование функций

  • Регистрация функций как обработчика

  • Аннотирование декораторов

  • Отладка и тестирование функций

  • Сопровождение и согласование кода

  • Кэширование и т. д.

И это ещё не всё. Я считаю, что теория декораторов со всей своей вложенностью похожа на цитату про рекурсию из книги «Грокаем Алгоритмы»:

«Они либо обожают её, либо ненавидят, либо ненавидят, пока не полюбят через пару-тройку лет.»

(Мне пришлось перечитать раздел про рекурсию, чтобы найти цитату, так как я никак не мог вспомнить как там дословно говорилось)

Подытожим:

  • Декораторы определяют блоки многократного использования, которые можно применять к вызываемой функции (и не только), для модификации его поведения без изменения самой функции (или другого объекта)

  • Синтаксис @ является всего-то сокращением вызова декоратора.

  • Многочисленные декораторы, размещенные над одной-единственной функцией, применяются снизу-вверх.

  • В качестве оптимального практического приема отладки надо использовать в своих собственных декораторах вспомогательный декоратор functools.wraps, чтобы не потерять метаданные из недекорированного вызываемого объекта в декорированный.

  • В теме декораторов ещё много интересной теории, по которой можно хоть отдельную книгу писать.


Источники:

«Грокаем Алгоритмы» Бхаргава Адитья

«Python декораторы на максималках. Универсальный рецепт по написанию и аннотированию от мала до велика»

«Чистый Python. Тонкости программирования для профи» Дэн Бейнер

«Изучаем Python» Марк Лутц

Комментарии (7)


  1. milssky
    27.05.2024 13:33
    +4

    Обмельчали нынче лиды и бекенд программисты (шутка)


    1. adron_s
      27.05.2024 13:33
      +4

      Да вроде как и не шутка :-) Декораторы это как бы основа основ в python. И не знать как они работают будучи лидом, это как минимум странно. Ладно там еще метаклассы, или дескрипторы, но декораторы...


      1. milssky
        27.05.2024 13:33
        +3

        Ну просто чтобы никого не обидеть. Все ж нежные :) А так да. Это база питона.

        Забавно видеть что у автора лидирующая позиция и он не подозревал о паттерне проектирования с именем "Декоратор".


        1. n1kkj Автор
          27.05.2024 13:33
          +3

          Я знал 95% этой информации до написания статьи, постоянно пользовался декораторами. Любопытно было почитать, что про них интересное пишут, тк знания о декораторах пришли как то сами, без особой теории. Вот написал краткую вырезку из того, что показалось мне полезным для изучения базы.

          Статейка создана как раз для людей, которым интересна такая начальная теория про декораторы)


          1. adron_s
            27.05.2024 13:33
            +7

            Понятно, тогда ждем аналогичную статью еще и про менеджеры контекста ;-)

            А вообще с этими декораторами магия какая то, в последнее время про них все начали писать / снимать видео. Вроде тема то простая совсем, но народу почему то не имется.


            1. olivera507224
              27.05.2024 13:33
              +2

              Не знаю зачем я тут вклиниваюсь, но всё же вклинюсь. Таки согласен. Не понимаю, почему людей так интересуют именно декораторы в питоне, ведь этот паттерн реализуем везде, где можно объявлять функции высшего порядка. Всё то же самое можно делать и в каком-нибудь условном JS, только без синтаксического сахара.

              Когда думаешьо декораторе просто как о функции, принимающей в качестве аргумента другую функцию, вся магия куда-то улетучивается.


  1. Mausglov
    27.05.2024 13:33
    +1

    разъяснение про "@" было полезно, спасибо. Когда-то читал статью про отладку в Python, там предлагалось логировать функции с помощью декоратора. И вот на этом месте возникало недоумение: "а как оно работает, когда декоратор не нужен?". Теперь понятно, что никак.