Изучаем продвинутые возможности Python, часть 2: замыкания, декораторы, модуль functools

В первой части серии публикаций о продвинутых возможностях Python мы познакомились с итераторами, генераторами и модулем itertools. В сегодняшней публикации речь пойдёт о замыканиях, декораторах и модуле functools.
Декораторы
Декоратор — паттерн проектирования, при использовании которого класс или функция изменяет или дополняет функциональность другого класса или функции без использования наследования или прямого изменения исходного кода. В Python декораторы представляют собой функции или любые вызываемые объекты, которые принимают на вход набор необязательных аргументов и функцию или класс и возвращают функцию или класс. Их можно использовать для реализации паттерна проектирования декоратора или для решения других задач. Декораторы классов появились в Python 2.6.
Кстати, если вы не знакомы с замыканиями Python, прежде чем читать дальше ознакомьтесь с дополнением о замыканиях в конце этой статьи. Концепцию декораторов сложно понять, если вы не знакомы с замыканиями.
В Python декораторы применяются к функции или классу с помощью символа @
. В качестве первого примера давайте используем простой декоратор, который регистрирует вызовы функций. В этом примере декоратор принимает формат времени в качестве аргумента и печатает лог перед и после выполнения декорированной функции с временем исполнения. Это может быть кстати, когда вы сравниваете эффективность разных реализаций алгоритма или разных алгоритмов.
Посмотрите на пример использования. Здесь функции add1
и add2
оформлены с помощью logged
, а также дан пример вывода. Заметьте, что формат времени хранится в замыкании возвращаемых функций с декоратором. Поэтому понимание замыканий необходимо для понимания декораторов Python.
Также обратите внимание, как имя возвращаемой функции заменяется именем оригинальной функции в случае, если оно используется позже. Python не делает этого по умолчанию.
Если вы достаточно внимательны, то заметите, что мы заботимся, чтобы у возвращаемой функции был правильно указан __name__
, но не заботимся о __doc__
или __module__
. Поэтому если у функции add
есть строка документации, она потеряется. Как можно этого избежать? Мы могли бы справиться с проблемой так же, как при обработке __name__
. Но выполнять такие операции с каждым декоратором утомительно. Поэтому в модуле functools
есть декоратор wraps
, который срабатывает именно в таком сценарии. Использование декоратора внутри другого декоратора может показаться странным. Но если вы думаете о декораторах как о функциях, которые принимают функции в качестве параметров и возвращают функции, всё становится на места. Декоратор wraps
используется в следующих примерах вместо ручной обработки __name__
и других подобных атрибутов.
Следующий пример немного сложнее. Давайте напишем декоратор, который кэширует результат вызова функции в течение указанного в секундах времени. Код ожидает, что переданные в функцию аргументы — хэшируемые объекты (hashable objects), потому что мы используем кортеж с аргументами args
в качестве первого параметра и замороженный набор элементов в kwargs
в качестве второго параметра, который выступает ключом кэша. У каждой функции будет уникальный кэш dict
, который хранится в замыкании функции.
Вот как это используется. Мы применяем декоратор к наивному и неэффективному калькулятору чисел Фибоначчи. Декоратор кэша эффективно применяет к коду паттерн мемоизации. Обратите внимание, что в замыкании fib
находятся кэш dict
, ссылка на исходную функцию fib
, значение аргумента logged
, а также значение аргумента timeout
. dump_closure
описывается в конце статьи после раздела о замыканиях.
Декораторы класса
В предыдущем разделе мы рассмотрели декораторы функций и некоторые необычные способы их применения. Теперь давайте рассмотрим декораторы классов. В данном случае декоратор принимает на вход класс (объект с типом type
в Python) и возвращает модифицированный класс.
Первый пример — простая математика. Дано частично упорядоченное множество P. Мы определяем Pd как дуальность P, исключительно если P(x,y)⟺Pd(y,x). Другими словами, речь идёт об обратном порядке. Как можно реализовать это с помощью Python? Предположим, класс определяет порядок с помощью методов __lt__
, __le__
и так далее. Тогда мы можем написать декоратор класса, который заменяет каждую функцию её дуальностью.
Вот как это можно применить к str
, чтобы создать новый класс rstr
, в котором используется обратный лексикографический порядок.
Давайте посмотрим на более сложный пример. Предположим, мы хотим применить декоратор logged
из предыдущего примера ко всем методам в классе. Это можно сделать вручную: просто добавить декоратор в каждый метод. Также можно автоматизировать процесс с помощью декоратора класса. Прежде чем сделать это, автор улучшил декоратор logged
из предыдущего раздела. Теперь в нём используется атрибут wraps
из модуля functools
вместо ручной работы с __name__
. Также здесь в возвращаемую функцию добавлен атрибут _logged_decorator
. Его значение True
, он применяется, чтобы избежать двойного декорирования функции. Это удобно, когда мы применяем декоратор к классам, которые должны наследовать методы от других классов. Наконец, добавлен аргумент name_prefix
, который делает возможной кастомизацию сообщений лога.
Теперь можно написать декоратор класса.
Вот как он будет использоваться. Обратите внимание, как здесь обрабатываются переопределённые методы и наследование.
Наш первый пример декораторов класса должен был изменять порядок методов класса. Похожий декоратор, но более полезный, может принимать один из __lt__
, __le__
, __gt__
или __ge__
и __eq__
, и реализовывать остальные для полного упорядочивания класса. Это именно то, что делает декоратор functools.total_ordering
. Подробности в документации.
Несколько примеров из Flask
Рассмотрим несколько интересных примеров использования декораторов в Flask.
Представьте, что хотите, чтобы некоторые функции выводили предупреждающие сообщения, если они вызываются при определённых обстоятельствах в режиме отладки. Вместо того, чтобы вручную добавлять код в начало каждой функции, можно использовать декоратор. Это то, что делает декоратор, который можно найти в файле app.py
Flask.
Более интересный пример — декоратор Flask route
, который определяется в классе Flask
. Заметьте, что декоратор может быть методом класса. В этом случае в качестве первого параметра используется self
. Полный код смотрите в файле app.py. Обратите внимание, декоратор просто регистрирует декорированную функцию как обработчик URL с помощью вызова функции add_url_rule
.
Дополнительное чтение
Много информации о декораторах вы найдёте на официальной вики-странице Python. Также можно посмотреть замечательное видео Дэвида Безли о метапрограммировании в Python 3.
Приложение: замыкания
Замыкание — это комбинация функции и множества ссылок на переменные в области видимости функции. Последнее иногда называют ссылочной средой. Замыкание позволяет выполнять функцию за пределами области видимости. В Python ссылочная среда хранится в виде набора ячеек. Доступ к ним можно получить с помощью атрибутов func_closure
или __closure__
. В Python 3 используется только __closure__
.
Важно понимать, что речь идёт просто о ссылках, а не о глубоких копиях объектов. Конечно, неважно, являются ли объекты неизменяемыми, но для изменяемых объектов, например, списков, это важно. Это иллюстрирует пример ниже. Обратите внимание, у функций также есть __globals__
, где хранится глобальное ссылочное окружение, для которого была определена функция. Посмотрите на простой пример:
Ещё один пример, более сложный. Убедитесь, что понимаете, почему код работает именно так.
Наконец, вот пример метода dump_closure
, который использовался выше.
Адаптированный перевод статьи A Study of Python's More Advanced Features Part II: Closures, Decorators and functools by Sahand Saba. Мнение автора оригинальной публикации может не совпадать с мнением администрации «Хекслета».