Pull to refresh

Декораторы в python по книгам

Level of difficultyEasy
Reading time7 min
Views2.6K

С 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» Марк Лутц

Tags:
Hubs:
Total votes 11: ↑7 and ↓4+5
Comments7

Articles