admin 30 июня 2018

Шаблоны проектирования в Python: для стильного кода

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

Шаблоны проектирования в Python

Python – это мощный динамический язык высокого уровня. Он обладает простым синтаксисом, отличной модульной структурой и огромным количеством удобных функций.

Python является объектно-ориентированным языком, однако прекрасно поддерживает функциональный стиль программирования. Разработчик вовсе не обязан создавать классы и их экземпляры. Если проекту не нужны сложные структуры, нет необходимости их строить. Можно просто писать функции или даже совсем не структурированный код, чтобы быстро выполнять несложные задачи. В то же время, все элементы языка – это объекты. Даже функции, которые являются "объектами первого класса".

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

Но поскольку Python настолько мощный и гибкий, разработчикам необходимы некоторые правила (или шаблоны) для программирования.

Подходит ли Python для паттернов?

Любой язык программирования подходит для паттернов, в том числе и Python.

Шаблоны программирования тесно связаны с контекстом их использования. Синтаксис и особенности конкретного языка задают собственные правила для разработчиков. Эти факторы в разных языках могут различаться и обычно не имеют конкретной цели – они, по большей части, технические. С другой стороны, те ограничения, которые связаны с использованием шаблонов, целенаправлены. Они рассказывают, как нужно делать и как не нужно.

Язык программирования Python

Философия Python базируется на хорошо продуманных лучших практиках программирования. Многие шаблоны проектирования уже встроены в язык. Разработчики используют их, даже не задумываясь. Ряд популярных паттернов очень легко реализовать благодаря динамической природе языка. А некоторые не используются в Python, так как в них нет необходимости.

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

Философия Python

У Python есть своя философия – Дзен. Она состоит из 19 простых утверждений:

Дзен Python

Это не шаблоны в традиционном смысле. Но эти правила определяют практичный и элегантный подход языка к программированию.

Еще есть PEP-8 (python enhanced proposal – заявки на улучшение языка python) – правила структурирования кода. Придерживаться их в работе очень важно, но, разумеется, есть некоторые исключения. Кстати, эти исключения поощряются самим PEP-8:

PEP8: философия Python

Смешайте PEP-8 с Дзен Python и получите идеальную основу для читаемого кода. Добавьте щепотку шаблонов проектирования. Теперь из этого теста можно создавать любые последовательные и легко изменяемые системы.

Что такое шаблоны проектирования?

Все началось с Банды четырех. Именно они сформулировали и подробно описали ряд способов решения распространенных проблем программирования. В их основу были положены два принципа:

  • Программировать нужно для интерфейсов, а не для конкретных реализаций.
  • Композицию следует предпочитать наследованию.

Рассмотрим, как они реализуются в Python.

Программирование для интерфейса

То, что в языке отсутствует ключевое слово interface, не означает, что он не соответствует принципам Банды четырех. Вспомните об утиной типизации. Это живой пример программирования для интерфейса.

Если нечто похоже на утку и крякает как утка, значит, это утка!

Утиная типизация в программировании

С утиной типизацией программа не беспокоится о сущности объекта. Она просто хочет знать, может ли объект делать то, что необходимо. То есть интересуется исключительно интерфейсом. Может ли объект крякать? Тогда пусть крякает!

try:
  bird.quack()
except AttributeError:
  self.lol()

В этой программе никакой интерфейс для утки не определяется. Но это отличный пример программирования для интерфейса, а не для конкретной реализации. Это очень удобно.

Композиция vs. Наследование

Композиция в 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)

Преимущества второго варианта очевидны. Экземпляр persister вводится прямо во время выполнения программы! Таким образом, сегодня это может быть реляционная база данных, а завтра что-то другое. Важно лишь, чтобы сохранялся необходимый интерфейс (опять эти надоедливые утки).

Поведенческие шаблоны

Эта группа решений объясняет, как организовывать связи между объектами. Банда четырех определила 11 моделей поведения. Среди них Итератор, Цепочка обязанностей и Команда.

Язык 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:

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

Решения этой группы позволяют скрывать логику создания объектов. Таким образом, можно получить экземпляр класса, не используя оператор new. Но в Python и так нет этого оператора!

Тем не менее, порождающие шаблоны можно реализовать средствами языка.

Одиночка

Шаблон Одиночка используется, если нужны гарантии, что существует единственный экземпляр данного класса. Во время выполнения программы не должны появляться другие. На самом деле, в Python проще намеренно создать один экземпляр, а затем использовать его.

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

Для решения той же задачи в Python есть ряд альтернатив:

  • использование модуля;
  • создание одного экземпляра на верхнем уровне приложения, например, в файле конфигурации;
  • передача экземпляра каждому объекту, которому он нужен.

Последнее решение – это инъекция зависимости. Ее тоже можно отнести к шаблонам проектирования.

Внедрение зависимости

Этот механизм можно отнести к группе порождающих. Он определяет, где и когда создается экземпляр класса. В сочетании с утиной типизацией – это мощный инструмент для организации связей в приложении.

Шаблоны проектирования в Python

Смысл шаблона в том, что зависимость производится в одном месте, а потребляется в другом. Код пользователя просто получает готовый внешний объект и использует его.

Если вам хочется пить, не нужно идти к холодильнику и брать из него напиток самостоятельно. Обозначьте свою потребность. Скажите родителям, что хотите выпить что-нибудь с обедом. Это неплохой пример внедрения зависимостей.

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()

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)

Узнать больше о мощном механизме инъекции зависимостей можно здесь и здесь.

Использование этого шаблона раскрывает большие возможности для модульного тестирования. Оно позволяет менять данные прямо на лету. Многое сразу становится проще, не так ли?

Структурные шаблоны

Эти паттерны занимаются объединением отдельных классов и объектов в сложные группы.

Фасад

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

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

Шаблон проектирования Фасад

Python предлагает очень элегантную реализацию шаблона.

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-сокет. Но объект сокета не имеет метода 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 – замечательный язык. Само его использование учит следовать лучшим практикам программирования. Их даже не обязательно осознавать, они интуитивны, словно являются второй натурой языка. Это ценят в нем и новички, и опытные разработчики.

Шаблон Декоратор позволяет расширять функциональность без использования наследования.

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()

Декорировать можно не только функции, но и целые классы. Единственное требование состоит в том, что они должны быть вызываемыми. Но в Python нет проблем с этим: нужно лишь определить метод __call __ (self).

Еще много интересного можно найти в модуле functools.

Вывод

Использовать шаблоны проектирования в Python очень легко. На нем вообще легко программировать. Недаром главная заповедь языка – «Простое лучше, чем сложное».

Обратите внимание, ни для одного из паттернов не приведена полномасштабная реализация. Их нужно «почувствовать» и реализовать оптимальным образом. Каким именно, зависит от стиля разработчика и потребностей проекта. А Python предоставит всю необходимую мощность для создания гибкого и многоразового кода.

Однако гибкость языка дает еще больше возможностей. Она позволяет писать действительно плохой код. Не делайте этого! Следуйте принципу DRY и не пишите строки длиной более 80 символов. Используйте шаблоны проектирования там, где они применимы. Это один из лучших способов учиться у других и бесплатно получать опыт.

Перевод статьи Andrei BoyanovPython Design Patterns: For Sleek And Fashionable Code

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
DevOps
Санкт-Петербург, от 150000 RUB до 400000 RUB
Продуктовый аналитик
Екатеринбург, по итогам собеседования
Продуктовый аналитик в поддержку
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ