Шаблоны дизайна Python: для гладкого и модного кода
Опубликовано: 2022-03-11Скажем еще раз: Python — это язык программирования высокого уровня с динамической типизацией и динамической привязкой. Я бы описал его как мощный динамический язык высокого уровня. Многие разработчики любят Python за его понятный синтаксис, хорошо структурированные модули и пакеты, а также за его огромную гибкость и набор современных функций.
В Python вас ничто не обязывает писать классы и создавать из них объекты. Если вам не нужны сложные структуры в вашем проекте, вы можете просто написать функции. Более того, вы можете написать плоский скрипт для выполнения какой-нибудь простой и быстрой задачи, вообще не структурируя код.
В то же время Python — это стопроцентно объектно-ориентированный язык. Как это? Проще говоря, все в Python является объектом. Функции — это объекты, объекты первого класса (что бы это ни значило). Тот факт, что функции являются объектами, важен, поэтому, пожалуйста, запомните его.
Итак, вы можете писать простые скрипты на Python или просто открывать терминал Python и тут же выполнять операторы (это так полезно!). Но при этом можно создавать сложные фреймворки, приложения, библиотеки и так далее. Вы можете сделать так много в Python. Есть, конечно, ряд ограничений, но это не тема данной статьи.
Однако, поскольку Python такой мощный и гибкий, нам нужны некоторые правила (или шаблоны) при программировании на нем. Итак, давайте посмотрим, что такое шаблоны и как они связаны с Python. Мы также перейдем к реализации нескольких основных шаблонов проектирования Python.
Почему Python хорош для шаблонов?
Любой язык программирования хорош для шаблонов. На самом деле шаблоны следует рассматривать в контексте любого данного языка программирования. И шаблоны, и синтаксис языка, и природа накладывают ограничения на наше программирование. Ограничения, проистекающие из синтаксиса и природы языка (динамический, функциональный, объектно-ориентированный и т. п.), могут различаться, как и причины их существования. Ограничения, исходящие от паттернов, существуют по какой-то причине, они целенаправленны. Это основная цель шаблонов; чтобы сказать нам, как делать что-то и как не делать этого. Мы поговорим о шаблонах, и особенно о шаблонах проектирования Python, позже.
Философия Python построена на идее хорошо продуманных лучших практик. Python — это динамический язык (я уже говорил это?), и поэтому он уже реализует или упрощает реализацию ряда популярных шаблонов проектирования с помощью нескольких строк кода. Некоторые шаблоны проектирования встроены в Python, поэтому мы используем их, даже не подозревая об этом. Другие шаблоны не нужны из-за природы языка.
Например, Factory — это структурный шаблон проектирования Python, направленный на создание новых объектов, скрывающий от пользователя логику инстанцирования. Но создание объектов в Python по своей природе является динамическим, поэтому такие дополнения, как Factory, не нужны. Конечно, вы можете реализовать его, если хотите. Могут быть случаи, когда это было бы действительно полезно, но это исключение, а не норма.
Что хорошего в философии Python? Давайте начнем с этого (исследуйте его в терминале Python):
> >> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
Это могут быть не шаблоны в традиционном смысле, но это правила, определяющие «питоновский» подход к программированию наиболее элегантным и полезным образом.
У нас также есть рекомендации по коду PEP-8, которые помогают структурировать наш код. Это необходимо для меня, за некоторыми исключениями, конечно. Кстати, эти исключения поощряются самим PEP-8:
Но самое главное: знайте, когда быть непоследовательным — иногда руководство по стилю просто не применимо. Если вы сомневаетесь, используйте свое лучшее суждение. Посмотрите на другие примеры и решите, что выглядит лучше. И не стесняйтесь спрашивать!
Объедините PEP-8 с The Zen of Python (также PEP — PEP-20), и вы получите идеальную основу для создания читаемого и поддерживаемого кода. Добавьте шаблоны проектирования, и вы готовы создавать программные системы любого типа с согласованностью и возможностью развития.
Шаблоны проектирования Python
Что такое шаблон проектирования?
Все начинается с Банды четырех (GOF). Сделайте быстрый онлайн-поиск, если вы не знакомы с GOF.
Шаблоны проектирования — распространенный способ решения хорошо известных проблем. В основе шаблонов проектирования, определенных GOF, лежат два основных принципа:
- Программа для интерфейса, а не реализация.
- Предпочитайте композицию объектов наследованию.
Давайте подробнее рассмотрим эти два принципа с точки зрения программистов Python.
Программа для интерфейса, а не реализация
Подумайте об утиной печати. В Python нам не нравится определять интерфейсы и программные классы в соответствии с этими интерфейсами, не так ли? Но, послушай меня! Это не значит, что мы не думаем об интерфейсах, на самом деле с Duck Typing мы делаем это постоянно.
Скажем несколько слов о печально известном подходе Duck Typing, чтобы увидеть, как он вписывается в эту парадигму: программа для интерфейса.
Нас не волнует природа объекта, нам не нужно заботиться о том, что это за объект; мы просто хотим знать, может ли он делать то, что нам нужно (нас интересует только интерфейс объекта).
Может ли объект крякать? Так пусть крякает!
try: bird.quack() except AttributeError: self.lol()
Мы определили интерфейс для нашей утки? Нет! Мы программировали интерфейс вместо реализации? Да! И я нахожу это таким милым.
Как отмечает Алекс Мартелли в своей известной презентации о шаблонах проектирования в Python: «Обучение уток печатать занимает некоторое время, но впоследствии избавляет вас от большого количества работы!»
Предпочитайте композицию объектов наследованию
Вот это я называю питоническим принципом! Я создал меньше классов/подклассов по сравнению с обертыванием одного класса (или чаще нескольких классов) в другой класс.
Вместо этого:
class User(DbObject): pass
Мы можем сделать что-то вроде этого:
class User: _persist_methods = ['get', 'save', 'delete'] def __init__(self, persister): self._persister = persister def __getattr__(self, attribute): if attribute in self._persist_methods: return getattr(self._persister, attribute)
Преимущества очевидны. Мы можем ограничить, какие методы обернутого класса выставлять. Мы можем внедрить экземпляр сохраняемого объекта во время выполнения! Например, сегодня это реляционная база данных, а завтра может быть что угодно, с нужным нам интерфейсом (опять же эти надоедливые утки).
Композиция элегантна и естественна для Python.
Поведенческие модели
Поведенческие паттерны включают в себя общение между объектами, то, как объекты взаимодействуют и выполняют заданную задачу. Согласно принципам GOF, всего в Python существует 11 поведенческих паттернов: Цепочка ответственности, Команда, Интерпретатор, Итератор, Посредник, Память, Наблюдатель, Состояние, Стратегия, Шаблон, Посетитель.
Я нахожу эти паттерны очень полезными, но это не значит, что другие группы паттернов бесполезны.
Итератор
Итераторы встроены в Python. Это одна из самых сильных характеристик языка. Несколько лет назад я где-то читал, что итераторы делают Python потрясающим, и я думаю, что это все еще так. Узнайте достаточно об итераторах и генераторах Python, и вы будете знать все, что вам нужно об этом конкретном шаблоне Python.
Цепочка ответственности
Этот шаблон дает нам возможность обрабатывать запрос, используя разные методы, каждый из которых относится к определенной части запроса. Знаете, один из лучших принципов хорошего кода — это принцип единой ответственности .
Каждый фрагмент кода должен делать одну и только одну вещь.
Этот принцип глубоко интегрирован в этот шаблон проектирования.
Например, если мы хотим отфильтровать какой-то контент, мы можем реализовать разные фильтры, каждый из которых выполняет один точный и четко определенный тип фильтрации. Эти фильтры можно использовать для фильтрации оскорбительных слов, рекламы, неприемлемого видеоконтента и т. д.
class ContentFilter(object): def __init__(self, filters=None): self._filters = list() if filters is not None: self._filters += filters def filter(self, content): for filter in self._filters: content = filter(content) return content filter = ContentFilter([ offensive_filter, ads_filter, porno_video_filter]) filtered_content = filter.filter(content)
Команда
Это один из первых шаблонов проектирования Python, который я реализовал как программист. Это напоминает мне: шаблоны не изобретаются, их обнаруживают . Они существуют, нам просто нужно их найти и использовать. Я обнаружил это для удивительного проекта, который мы реализовали много лет назад: редактор WYSIWYM XML специального назначения. После интенсивного использования этого паттерна в коде я прочитал о нем больше на некоторых сайтах.
Шаблон команды удобен в ситуациях, когда по какой-то причине нам нужно начать с подготовки того, что будет выполняться, а затем выполнить это, когда это необходимо. Преимущество заключается в том, что такая инкапсуляция действий позволяет разработчикам Python добавлять дополнительные функции, связанные с выполняемыми действиями, такие как отмена/повтор, сохранение истории действий и тому подобное.
Давайте посмотрим, как выглядит простой и часто используемый пример:
class RenameFileCommand(object): def __init__(self, from_name, to_name): self._from = from_name self._to = to_name def execute(self): os.rename(self._from, self._to) def undo(self): os.rename(self._to, self._from) class History(object): def __init__(self): self._commands = list() def execute(self, command): self._commands.append(command) command.execute() def undo(self): self._commands.pop().undo() history = History() history.execute(RenameFileCommand('docs/cv.doc', 'docs/cv-en.doc')) history.execute(RenameFileCommand('docs/cv1.doc', 'docs/cv-bg.doc')) history.undo() history.undo()
Творческие шаблоны
Начнем с того, что отметим, что порождающие шаблоны обычно не используются в Python. Почему? Из-за динамической природы языка.
Кто-то мудрее меня однажды сказал, что Factory встроена в Python. Это означает, что сам язык предоставляет нам всю гибкость, необходимую для создания объектов достаточно элегантным способом; нам редко нужно реализовывать что-то сверху, например, Singleton или Factory.
В одном руководстве по Python Design Patterns я нашел описание создающих шаблонов проектирования, в котором говорилось, что эти шаблоны проектирования предоставляют способ создавать объекты, скрывая логику создания, а не создавать экземпляры объектов напрямую с помощью оператора new .

Это в значительной степени подводит итог проблеме: у нас нет нового оператора в Python!
Тем не менее, давайте посмотрим, как мы можем реализовать некоторые из них, если мы чувствуем, что можем получить преимущество, используя такие шаблоны.
Синглтон
Шаблон Singleton используется, когда мы хотим гарантировать, что во время выполнения существует только один экземпляр данного класса. Нам действительно нужен этот шаблон в Python? Исходя из моего опыта, проще просто намеренно создать один экземпляр, а затем использовать его вместо реализации шаблона Singleton.
Но если вы захотите его реализовать, вот хорошая новость: в Python мы можем изменить процесс создания экземпляров (наряду практически со всем остальным). Помните метод __new__()
, о котором я упоминал ранее? Вот так:
class Logger(object): def __new__(cls, *args, **kwargs): if not hasattr(cls, '_logger'): cls._logger = super(Logger, cls ).__new__(cls, *args, **kwargs) return cls._logger
В этом примере Logger является синглтоном.
Это альтернативы использованию Singleton в Python:
- Используйте модуль.
- Создайте один экземпляр где-нибудь на верхнем уровне вашего приложения, возможно, в файле конфигурации.
- Передайте экземпляр каждому объекту, который в нем нуждается. Это внедрение зависимостей, и это мощный и простой в освоении механизм.
Внедрение зависимости
Я не собираюсь вдаваться в дискуссию о том, является ли внедрение зависимостей шаблоном проектирования, но скажу, что это очень хороший механизм реализации слабой связи, который помогает сделать наше приложение удобным в сопровождении и расширяемым. Объедините это с Duck Typing, и Сила будет с вами. Всегда.
Я перечислил его в разделе шаблонов создания этого поста, потому что он касается вопроса о том, когда (или даже лучше: где) создается объект. Он создан снаружи. Лучше сказать, что объекты вообще не создаются там, где мы их используем, поэтому зависимость не создается там, где она потребляется. Код потребителя получает созданный извне объект и использует его. Для получения дополнительной справки, пожалуйста, прочитайте ответ на этот вопрос Stackoverflow, получивший наибольшее количество голосов.
Это хорошее объяснение внедрения зависимостей, которое дает нам хорошее представление о потенциале этого конкретного метода. По сути, ответ объясняет проблему на следующем примере: Не берите напитки из холодильника самостоятельно, вместо этого заявите о потребности. Скажи родителям, что тебе нужно что-нибудь выпить за обедом.
Python предлагает нам все необходимое для простой реализации. Подумайте о его возможной реализации на других языках, таких как Java и C#, и вы быстро осознаете всю прелесть Python.
Давайте подумаем о простом примере внедрения зависимостей:
class Command: def __init__(self, authenticate=None, authorize=None): self.authenticate = authenticate or self._not_authenticated self.authorize = authorize or self._not_autorized def execute(self, user, action): self.authenticate(user) self.authorize(user, action) return action() if in_sudo_mode: command = Command(always_authenticated, always_authorized) else: command = Command(config.authenticate, config.authorize) command.execute(current_user, delete_user_action)
Мы внедряем методы аутентификатора и авторизатора в класс Command. Все, что нужно классу Command, — это успешно выполнить их, не заморачиваясь деталями реализации. Таким образом, мы можем использовать класс Command с любыми механизмами аутентификации и авторизации, которые мы решим использовать во время выполнения.
Мы показали, как внедрять зависимости через конструктор, но мы можем легко внедрить их, установив непосредственно свойства объекта, раскрывая еще больший потенциал:
command = Command() if in_sudo_mode: command.authenticate = always_authenticated command.authorize = always_authorized else: command.authenticate = config.authenticate command.authorize = config.authorize command.execute(current_user, delete_user_action)
О внедрении зависимостей можно узнать гораздо больше; например, любопытные люди будут искать IoC.
Но прежде чем вы это сделаете, прочитайте другой ответ Stackoverflow, за который больше всего голосовали на этот вопрос.
Опять же, мы только что продемонстрировали, как реализация этого замечательного шаблона проектирования на Python — это всего лишь вопрос использования встроенных функций языка.
Давайте не будем забывать, что все это означает: метод внедрения зависимостей позволяет проводить очень гибкое и простое модульное тестирование. Представьте себе архитектуру, в которой вы можете изменять хранение данных на лету. Насмешка над базой данных становится тривиальной задачей, не так ли? Для получения дополнительной информации вы можете ознакомиться с Toptal Introduction to Mocking in Python.
Вы также можете изучить шаблоны проектирования Prototype , Builder и Factory .
Структурные модели
Фасад
Возможно, это самый известный шаблон проектирования Python.
Представьте, что у вас есть система со значительным количеством объектов. Каждый объект предлагает богатый набор методов API. С этой системой можно делать много чего, но как насчет упрощения интерфейса? Почему бы не добавить объект интерфейса, предоставляющий хорошо продуманное подмножество всех методов API? Фасад!
Пример шаблона проектирования Python Facade:
class Car(object): def __init__(self): self._tyres = [Tyre('front_left'), Tyre('front_right'), Tyre('rear_left'), Tyre('rear_right'), ] self._tank = Tank(70) def tyres_pressure(self): return [tyre.pressure for tyre in self._tyres] def fuel_level(self): return self._tank.level
Нет ни удивления, ни подвоха, класс Car
— это Фасад , и все.
Адаптер
Если фасады используются для упрощения интерфейса, то адаптеры предназначены для изменения интерфейса. Это как использовать корову, когда система ожидает утку.
Допустим, у вас есть рабочий метод для регистрации информации в заданном месте. Ваш метод ожидает, что у пункта назначения будет метод write()
(как, например, у каждого файлового объекта).
def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message))
Я бы сказал, что это хорошо написанный метод с внедрением зависимостей, который обеспечивает большую расширяемость. Скажем, вы хотите войти в какой-то сокет UDP, а не в файл, вы знаете, как открыть этот сокет UDP, но единственная проблема заключается в том, что объект socket
не имеет метода write()
. Вам нужен адаптер !
import socket class SocketWriter(object): def __init__(self, ip, port): self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._ip = ip self._port = port def write(self, message): self._socket.send(message, (self._ip, self._port)) def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message)) upd_logger = SocketWriter('1.2.3.4', '9999') log('Something happened', udp_destination)
Но почему я считаю адаптер таким важным? Что ж, когда это эффективно сочетается с внедрением зависимостей, это дает нам огромную гибкость. Зачем изменять наш хорошо протестированный код для поддержки новых интерфейсов, если мы можем просто реализовать адаптер, который преобразует новый интерфейс в хорошо известный?
Вы также должны проверить и освоить шаблоны проектирования мостов и прокси из-за их сходства с адаптером . Подумайте, как легко их реализовать на Python, и подумайте, как вы можете использовать их в своем проекте.
Декоратор
О, как нам повезло! Декораторы действительно хороши, и мы уже интегрировали их в язык. Что мне больше всего нравится в Python, так это то, что его использование учит нас использовать лучшие практики. Дело не в том, что мы не должны осознавать лучшие практики (и, в частности, шаблоны проектирования), но с Python я чувствую, что следую лучшим практикам, несмотря ни на что. Лично я считаю, что лучшие практики Python интуитивно понятны и являются второй натурой, и это ценят как начинающие, так и опытные разработчики.
Шаблон декоратора предназначен для введения дополнительных функций и, в частности, без использования наследования.
Итак, давайте посмотрим, как мы декорируем метод, не используя встроенную функциональность Python. Вот простой пример.
def execute(user, action): self.authenticate(user) self.authorize(user, action) return action()
Что здесь не так хорошо, так это то, что функция execute
делает гораздо больше, чем что-то выполняет. Мы не следуем принципу единой ответственности буквально.
Было бы хорошо просто написать следующее:
def execute(action): return action()
Мы можем реализовать любые функции авторизации и аутентификации в другом месте, в декораторе , вот так:
def execute(action, *args, **kwargs): return action() def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizeddError return decorated execute = authenticated_only(execute) execute = authorized_only(execute)
Теперь метод execute()
:
- Легко читать
- Делает только одну вещь (по крайней мере, если смотреть на код)
- Украшен аутентификацией
- Украшен авторизацией
Мы пишем то же самое, используя синтаксис встроенного декоратора Python:
def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs ) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizedError return decorated @authorized_only @authenticated_only def execute(action, *args, **kwargs): return action()
Важно отметить, что вы не ограничены функциями декораторов. Декоратор может включать целые классы. Единственное требование состоит в том, что они должны быть вызываемыми . Но у нас нет проблем с этим; нам просто нужно определить метод __call__(self)
.
Вы также можете поближе познакомиться с модулем functools Python. Там есть что открыть!
Заключение
Я показал, насколько естественно и легко использовать шаблоны проектирования Python, но я также показал, что программирование на Python также должно быть легким.
«Простое лучше сложного», помните это? Возможно, вы заметили, что ни один из шаблонов проектирования не описан полностью и формально. Никаких сложных полномасштабных реализаций показано не было. Вам нужно «почувствовать» и реализовать их так, как это лучше всего соответствует вашему стилю и потребностям. Python — отличный язык, и он дает вам все возможности, необходимые для создания гибкого и многократно используемого кода.
Тем не менее, это дает вам больше, чем это. Это дает вам «свободу» писать очень плохой код. Не делай этого! Не повторяйтесь (DRY) и никогда не пишите строки кода длиннее 80 символов. И не забывайте использовать шаблоны проектирования там, где это применимо; это один из лучших способов бесплатно учиться у других и использовать их богатый опыт.