Перевод статьи Advanced Design Patterns in Python
Цель данной статьи - показать передовые паттерны в Python и лучший способ их использовать. В зависимости от того, что вам нужно от структуры кода, будь то быстрый поиск, постоянство, индексация и т.д., вы можете выбрать оптимальную структуру для работы, и большую часть времени вы будете смешивать их вместе что бы получить логичную и простую для понимания модель данных. Паттерны в Python очень интуитивны с точки зрения синтаксиса и они предлагают большой выбор операций. Это руководство пытается собрать наиболее распространенную и полезную информацию о каждом паттерне, а так же советы о том когда лучше всего использовать тот или иной паттерн.
Генераторы
Если вы очень долго используете Python, скорее всего вы слышали о генераторах списков. Они подходят для цикла, блока if и способны уместить все это в одну строку. Другими словами, вы можете отобразить (map) и отфильтровать (filter) список одним выражением.
Генератор списка состоит из следующих частей:
- входная последовательность
- переменная, представляющая элементы входной последовательности
- условие (опционально)
- выходное выражение, полученное из элементов входного списка, которые удовлетворяют условию
Допустим, нам нужно получить список всех квадратов целых чисел, которые больше нуля:
1 2 3 4 5 6 7 8 9 |
|
Согласитесь, довольно просто? Но это занимает 4-е строчки, 2-у уровня вложенности, и вдобавок мы делаем довольно тривиальную вещь. Вы можете уменьшить количество кода с помощью функций filter, lambda и map:
1 2 3 4 5 |
|
Теперь код расширяется горизонтально! Что можно сделать, что бы упростить код? Применить генераторы списков.
1 2 3 4 5 |
|
Генератор списков заключен в квадратные скобки, таким образом, видно что список производится сразу. В этом генераторе списка только один вызов функции и нет вызовов загадочной lambda - используется обычный итератор, выходное выражение и опциональное условие.
Но, есть и минусы: результирующий списов вычисляется и сохраняется в память сразу. Это не проблема для небольших списков, наподобие приведенных выше, или даже списков на порядок больших. Но иногда это может быть неэффективно.
Генераторы выручат и сейчас. Выражение-генератор не загружает весь список в память сразу, а вместо этого создает объект генератора, поэтому за один раз можно получить только один элемент.
Выражения-генераторы имеют синтаксис, похожий на синтаксис генераторов списков, только вместо квадратных скобок - круглые:
1 2 3 4 5 6 7 8 9 10 |
|
Это даже немного эффективнее использования генераторов списков. Заменим пример более эффективным кодом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Использование выражения-генератора вероятно более хорошая практика, но вы не увидите разницы в эффективности, если список не очень велик.
Так же, вы можете использовать функцию zip для работы с двумя и более элементами за раз:
1 2 3 4 5 6 7 8 9 |
|
Пример двухуровневого генератора с использованием os.walk():
1 2 3 4 5 6 7 8 |
|
Декораторы
Декораторы предоставляют очень удобный метод для добавления функциональности для существующих функций и классов. Похоже на АОП (аспектно-ориентированное программирование) в Java, не так ли? Кроме того, что это проще, а значит мощнее. Например, предположим вам нужно что-либо сделать на точках входа и выхода их функции (защиту, отслеживание, блокирование и др. - стандартные элементы АОП).
Декоратор - это функция, которая обертывает другую функцию: вызывается основная функция и ее результат передается в декоратор. Затем декоратор возвращает функцию, которая заменяет оборачиваемую функцию так как нужно.
1 2 3 4 5 6 7 8 9 10 |
|
Символ @ указывает на применение декоратора.
Теперь давайте реализуем код декоратора. Здесь мы фактически используем код декорируемой функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Когда вы пишете код вроде этого:
1 2 |
|
это то же самое, как если бы выполнялось следующее
1 2 3 4 |
|
Код внитри декоратора обычно включает в себя создание новой функции, которая принимает любые аргументы (с использованием *args и **kwargs) как показано в случае с функцией wrapper в этом примере. Внутри этой функции вы принимает входные аргументы оригинальной функции и возвращаете результат. Однако, вы так же можете разместить там дополнительный код (например, замер времени и т.д.). Таким образов созданная функция-обертка возвращает результат, как если бы это была оригинальная функция.
Давайте рассмотрим другой пример:
1 2 3 |
|
Когда компилятор проходит этот код, function()
компилируется и получившийся объект-функция передается в код декоратора decorator
, который создает из нее другой объект-функцию, что бы заменить первоначальную функцию function()
.
Как выглядит код декоратора? В основном в примерах показывают его как функцию, но я обнаружил, что легче разобраться в декораторах с помощью классов. Кроме того, классы дают больше возможностей.
Единственное ограничение на результат возвращаемый декоратором, это то, что он может быть вызван как функция - т.е. что его можно вызвать. Таким образом, любые классы, которые мы используем в качестве декораторов должны быть с методом __call__
.
Что должен делать декоратор после вызова? Вообще-то, все что угодно, но обычно ожидается, что будет использован код оригинальной функции. Однако, это не обязательно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Практический пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
ContextLib (менеджеры контекста)
Модуль contextlib содержит средства для работы с менеджерами контекста и оператором with. Обычно, что бы написать менеджер контекста, вы определяете класс с методами __enter__()
и __exit__()
. Например:
1 2 3 4 5 6 7 8 9 10 11 |
|
Полный пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Менеджер контекста “включается” оператором with. Возвращаемый объект будет использоваться в контексте. Метод __enter__()
выполняется, когда поток управления входит в блок кода внутри оператора with. Когда поток управления покидает блок кода внутри with, вызывается метод __exit__()
, что бы очистить используемые ресурсы.
Заново напишем исходный пример, используя декоратор @contextmanager
из модуля contextlib:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
В функции demo(label)
весь код до оператора yield исполняется как метод менеджера контекста __enter__()
. Весь код после оператора yield выполняется как метод __exit__()
. Если в блоке внутри with возникнет исключение, оно “объявится” на месте оператора yield.
Дескрипторы
Дескрипторы определяют как осуществляется доступ к аттрибутам объекта. Дескриптор является способом изменить то, что происходит, когда вы обращаетесть к аттрибуту объекта.
Что бы создать дескриптор, нужно определить хотя бы один из следующих трех методов. Обратите внимание, что instance
- это объект, к аттрибуту которого меняется доступ, а owner
- класс, для которого дескриптор является аттрибутом.
__get__(self, instance, owner)
- вызывается, когда запрашивается аттрибут (value = obj.attr); то что возвращается будет передано коду, запрашивающему аттрибут.
__set__(self, instance, value)
- вызывается, когда аттрибуту устанавливается значение (obj.attr = value); ничего не возвращает.
__delete__(self, instance)
- вызывается, когда аттрибут объекта удалаяется (del obj.attr)
Ленивые аттрибуты:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Дескрипторы - это обобщение концепции связанных методов, лежащих в основе реализации классических классов. В классических классах, когда аттрибут экзампляра не найден в словаре экземпляра, поиск продолжается в словаре класса, а затем рекурсивно в словарях базовых классов. Когда аттрибут найден в словаре класса (не в словаре экземпляра), интерпретатор проверяет, является ли найденный объект функцией. Если это так, то возвращается не найденный объект, а обернутый объект, который действует как каррированая функция. Когда вызывается обернутый объект, он вызывает оригинальную функцию с экземпляром в качестве одного из аргументов.
Как отмечено выше, дескрипторы закрепляются за классом, и когда осуществляется доступ к аттрибуту, автоматически вызываются специальные методы, причем используемый метод зависит от того, какой типа доступа осуществляется.
Метаклассы
Метаклассы предлагают мощный способ изменить поведение классов в Python.
Метакласс определяется как “класса класса”. Любой класс, экземпляры которого являются сами классы, является метаклассом.
1 2 3 4 5 6 7 8 9 10 |
|
Мы создали класс и объект этого класса. Запросив у экземпляра аттрибут __class__
, мы увидели, что это demo
. Дальше интереснее. Что такое класс demo
? У него мы тоже можем посмотреть аттрибут __class__
- это type
.
Итак, type
- это класс классов в Python. Другими словами, в приведенном выше примере obj
- это объект класса demo
, сам класс demo
является объектом type
.
Таким образом это делает type
метаклассом - в действительности наиболее часто используемый метакласс в Python, т.к. это дефолтный метакласс для всех классов.
Т.к. метакласс - это класс классов, он используется для создания классов (тех, что создают объекты). Но подождите, не мы ли создаем классы, когда определяем их стандартным способом? Все верно, но то что делает Python “под капотом” выглядит так:
- когда встречается определение класса, Python собирает аттрибуты (включая методы) в словарь
- когда определение класса закончилось, Python определяет для него метакласс; давайте назовем его Meta
- после Python выполняет
Meta(name, bases, dct)
, где:Meta
- это метакласс, поэтому этот вызов создает его экземплярname
- это имя только что созданного классаbases
- это кортеж базовых классовdct
- словарь, связывающий названия аттрибутов с объектами; в нем перечислены все аттрибуты класса
Как определить какой метакласс у класса? Если у класса (или один из его базовых классов) имеет аттрибут __metaclass__
, то он считается метаклассом. В противном случае, метаклассом является type.
Паттерны
“Проще просить прощения, чем разрешения”
Один из принципов Python - “Проще просить прощения, чем разрешения”. В отличие от подхода “семь раз отмерь”, этот принцип заключается в том, что сначала вы должны попытаться выполнить действие и если возникает ошибка - реагировать соответствующим образом. Продвинутая обработка исключений в Python поддерживает этот принцип и помогает разрабатывать надежные и устойчивые программы.
Синглтон (одиночка)
Синглтон - это объекты, предполагающие наличие только одного экземпляра. Python предоставляет несколько путей для реализации синглтонов.
Null object
Null object может быть использован вместо None, что бы избежать проверки на None.
Обозреватель
Паттерн обозреватель позволяет нескольким объектам иметь доступ к общим данным.
Конструктор
Параметры конструктора часто назначаются переменным экземпляра. Этот паттерн может заменить много строк ручного присваивания одной строчкой.
Заключение
Спасибо за чтение. Оставляйте свои комментарии для дальнейшего обсуждения.