Рубріки: Теория

Python Design Patterns: руководство для понятного и модного кода

Игорь Грегорченко

Python — это мощный объектно-ориентированный язык программирования высокого уровня с динамической типизацией и связыванием. Благодаря его гибкости и мощности разработчики часто используют определенные правила, или паттерны проектирования Python. Что делает их такими важными и что это значит для рядового разработчика Python? В этом посте мы объясняем новичкам, почему Python отлично подходит для паттернов проектирования, и как их можно использовать для раскрытия еще большего потенциала или для оптимизации разработки (и повышения удобства сопровождения кода).

Давайте повторим это еще раз: 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 поведенческих паттернов:

Цепочка ответственности, Команда, Интерпретатор, Итератор, Медиатор, Хранитель (англ. Memento), Наблюдатель, Состояние, Стратегия, Шаблон, Посетитель.

Я считаю эти паттерны очень полезными, но это не значит, что другие группы паттернов не полезны.

ИТЕРАТОР

Итераторы встроены в 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 я нашел описание паттернов проектирования создания, в котором говорится, что эти «паттерны обеспечивают способ создания объектов, скрывая логику создания, а не инстанцируя объекты напрямую с помощью оператора new».

Это в значительной степени суммирует проблему: у нас нет оператора 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 является синглтоном.

Вот альтернативные варианты использования синглтона в 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 — это всего лишь вопрос использования встроенных функций языка.

Давайте не будем забывать, что все это значит: техника инъекции зависимостей позволяет проводить очень гибкое и простое модульное тестирование. Представьте себе архитектуру, в которой вы можете менять хранилище данных «на лету». Издевательство над базой данных становится тривиальной задачей, не так ли?

Вы также можете изучить дополнительные шаблоны проектирования Prototype, Builder и Factory.

Структурные паттерны

FACADE

Это, возможно, самый известный паттерн проектирования Python.

Представьте, что у вас есть система с большим количеством объектов. Каждый объект предлагает богатый набор методов API. Вы можете многое сделать с этой системой, но как насчет упрощения интерфейса? Почему бы не добавить интерфейсный объект, раскрывающий хорошо продуманное подмножество всех методов API? Фасад!

Фасад — это элегантный паттерн проектирования Python. Это отличный способ упорядочить интерфейс.

Пример паттерна проектирования 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-сокет, но проблема в том, что у объекта сокета нет метода 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)

Но почему я считаю адаптер таким важным? Когда он эффективно сочетается с инъекцией зависимостей, это дает нам огромную гибкость. Зачем изменять наш хорошо протестированный код для поддержки новых интерфейсов, если можно просто реализовать адаптер, который преобразует новый интерфейс в хорошо известный?

Вам также следует ознакомиться и освоить паттерны проектирования bridge и proxy из-за их сходства с адаптером. Подумайте, насколько легко они реализуются в 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).

Возможно, вы также захотите поближе познакомиться с модулем Python functools. Там есть много интересного!

Заключение

Я показал, насколько естественно и просто использовать паттерны проектирования Python, но я также показал, что программирование на Python тоже должно быть простым.

«Простое лучше сложного», помните это? Возможно, вы заметили, что ни один из паттернов проектирования не был полностью и формально описан. Не было показано никаких сложных полномасштабных реализаций. Вы должны «почувствовать» и реализовать их так, как это лучше всего соответствует вашему стилю и потребностям. Python —   отличный язык, и он дает вам всю необходимую мощь для создания гибкого и многократно используемого кода.

Однако он дает вам не только это. Он дает вам «свободу» писать действительно плохой код. Не делайте этого! Не повторяйте себя (принцип DRY) и никогда не пишите строки кода длиннее 80 символов (принцип KISS). И не забывайте использовать паттерны проектирования там, где это применимо; это один из лучших способов учиться у других и бесплатно использовать их богатый опыт.

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023