Изучаем продвинутые возможности Python, часть 2: замыкания, декораторы, модуль functools
В первой части серии публикаций о продвинутых возможностях Python мы познакомились с итераторами, генераторами и модулем itertools. В сегодняшней публикации речь пойдёт о замыканиях, декораторах и модуле functools.
Содержание
- Декораторы
- Декораторы класса
- Несколько примеров из Flask
- Дополнительное чтение
- Приложение: замыкания
Декораторы
Декоратор — паттерн проектирования, при использовании которого класс или функция изменяет или дополняет функциональность другого класса или функции без использования наследования или прямого изменения исходного кода. В 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. Мнение автора оригинальной публикации может не совпадать с мнением администрации «Хекслета».
Дмитрий Дементий
6 лет назад
12



.png)





