Share This
Связаться со мной
Крути в низ
Categories
//🐍 3 лучших паттерна проектирования в Python: синглтон, декоратор и итератор

🐍 3 лучших паттерна проектирования в Python: синглтон, декоратор и итератор

3 luchshih patterna proektirovanija v python singlton dekorator i iterator 3ec2c4c - 🐍 3 лучших паттерна проектирования в Python: синглтон, декоратор и итератор

Kaggle expert⚛️ Пишу материал о различных алгоритмах и техниках в сфере Machine Learning. Паттерны в Python – это шаблоны для решения задач, которые часто встречаются в практике программиста. Они представляют из себя огромный набор инструментов. В этом материале вы познакомитесь с самыми главными из них.

3 luchshih patterna proektirovanija v python singlton dekorator i iterator 3b1bbcd - 🐍 3 лучших паттерна проектирования в Python: синглтон, декоратор и итератор

Благодаря книге «Паттерны проектирования: Elements of Reusable Object-Oriented Softwar» (авторы Эрих Гамма, Ричард Хелм, Ральф Джонсон и Джон Влиссидес), шаблоны (паттерны) приобрели популярность в компьютерной науке. В отрасли ее называют Gangs of Four – «Банда четырех».

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

Большая часть книги посвящена паттернам для языка программирования Java и C++. Однако, в этой статье мы сделаем упор на использование паттернов в языке Python. Рассмотрим несколько шаблонов проектирования из каждой категории, согласно изначально предложенной классификации внутри книги, которые показались мне наиболее интересными в контексте программирования на Python.

Что такое паттерн проектирования?

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

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

Классификация паттернов проектирования

Изначально существовало две основные классификации паттернов проектирования:

  1. Какую проблему решает паттерн.
  2. Как относится паттерн к классам или объектам.

Принимая во внимание первую классификацию, паттерны можно разделить на три группы:

  1. Порождающие – предоставляют возможность создания контролируемым образом, инициализации и конфигурации объектов, классов и типов данных на основе требуемых критериев.
  2. Структурные – помогают организовать структуры связанных объектов и классов, предоставляя новые функциональные возможности.
  3. Поведенческие – направлены на выявление общих моделей взаимодействия между объектами.

Позже появились новые паттерны проектирования, из которых можно выделить еще одну категорию:

Concurrency (параллелизм) – это тот тип паттернов проектирования, который имеет дело с многопоточной парадигмой программирования.

Паттерн 1: Синглтон

Синглтон (одиночка) – это паттерн проектирования, цель которого ограничить возможность создания объектов данного класса одним экземпляром. Он обеспечивает глобальность до одного экземпляра и глобальный доступ к созданному объекту.

Примеры использования

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

Пример кода:

Первый наивный подход (naive approach):

         class Logger:    @staticmethod    def get_instance():        if '_instance' not in Logger.__dict__:            Logger._instance = Logger()        return Logger._instance     def write_log(self, path):        pass   if __name__ == "__main__":    s1 = Logger.get_instance()    s2 = Logger.get_instance()    assert s1 is s2     

Что не так с этим кодом?

Он нарушает принцип единственной ответственности и имеет нестандартный доступ к классу. Необходимо помнить, что доступ к экземплярам класса осуществляется только методом get_instance(). Попытаемся исправить эти проблемы в следующем примере кода:

         class Singleton:   _instances = {}   def __new__(cls, *args, **kwargs):       if cls not in cls._instances:           instance = super().__new__(cls)           cls._instances[cls] = instance       return cls._instances[cls] class Logger(Singleton):   def write_log(self, path):       pass if __name__ == "__main__":   logger1 = Logger()   logger2 = Logger()   assert logger1 is logger2     

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

Давайте попробуем.

         class Singleton(type):   _instances = {}   def __call__(cls, *args, **kwargs):       if cls not in cls._instances:           instance = super().__call__(*args, **kwargs)           cls._instances[cls] = instance       return cls._instances[cls] class Logger(metaclass=Singleton):   def write_log(self, path):       pass if __name__ == "__main__":   logger1 = Logger()   logger2 = Logger()   assert logger1 is logger2     

Все работает. Однако, надо сделать еще одну настройку – подготовить программу к работе в многопоточной среде.

         from threading import Lock, Thread class Singleton(type):   _instances = {}   _lock: Lock = Lock()   def __call__(cls, *args, **kwargs):       with cls._lock:           if cls not in cls._instances:               instance = super().__call__(*args, **kwargs)               cls._instances[cls] = instance       return cls._instances[cls] class Logger(metaclass=Singleton):   def __init__(self, name):       self.name = name   def write_log(self, path):       pass def test_logger(name):   logger = Logger(name)   print(logger.name) if __name__ == "__main__":   process1 = Thread(target=test_logger, args=("FOO",))   process2 = Thread(target=test_logger, args=("BAR",))   process1.start()   process2.start()     

Вывод:

         FOO FOO     

Подведем итоги. Особенности использования Синглтона:

  • Класс имеет только один экземпляр;
  • Вы получаете глобальную точку доступа к этому экземпляру;
  • Синглтон инициализируется только при первом запросе;
  • Маскирует плохой дизайн до определенного момента. Это одна из причин, почему многие считают синглтон антипаттерном.

Паттерн 2: Декоратор

Декоратор – это структурный паттерн. Цель которого – предоставление новых функциональных возможностей классам и объектам во время выполнения кода.

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

Случаи использования

  • Необходимость назначить дополнительные обязанности объектам во время выполнения, не ломая код, который использует эти объекты;
  • По каким-то причинам невозможно расширить «цепочку обязанностей» объекта через наследование.

Пример кода

Используя декораторы, вы можете обернуть объекты несколько раз, поскольку и цель, и декораторы реализуют один и тот же интерфейс.

Получаемый объект будет обладать объединенной и сложенной функциональностью всех декораторов.

         from abc import ABC, abstractmethod class Component(ABC):   @abstractmethod   def operation(self):       pass class ConcreteComponent(Component):   def operation(self):       return "ConcreteComponent" class Decorator(Component):   def __init__(self, component):       self.component = component   @abstractmethod   def operation(self):       pass class ConcreteDecoratorA(Decorator):   def operation(self):       return f"ConcreteDecoratorA({self.component.operation()})" class ConcreteDecoratorB(Decorator):   def operation(self):       return f"ConcreteDecoratorB({self.component.operation()})" if __name__ == "__main__":   concreteComponent = ConcreteComponent()   print(concreteComponent.operation())   decoratorA = ConcreteDecoratorA(concreteComponent)   decoratorB = ConcreteDecoratorB(decoratorA)   print(decoratorB.operation())       

Вывод:

         ConcreteComponent ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))     

Практичный пример с использованием встроенного механизма декораторов:

         import sys def memoize(f):   cache = dict()   def wrapper(x):       if x not in cache:           cache[x] = f(x)       return cache[x]   return wrapper @memoize def fib(n):   if n <= 1:       return n   else:       return fib(n - 1) + fib(n - 2) if __name__ == "__main__":   sys.setrecursionlimit(2000)   print(fib(750))     

Вывод:

         2461757021582324272166248155313036893697139996697461509576233211000055607912198979704988704446425834042795269603588522245550271050495783935904220352228801000     

Без использования декоратора кэша для функции, которая рекурсивно вычисляет n-й член ряда Фибоначчи, трудно вычислить результат для значения 100 за все время работы.

Подведем итоги. Возможности декоратора:

  • Расширение поведения объекта без создания подкласса;
  • Добавление или удаление обязанности объекта во время выполнения;
  • Объединение нескольких моделей поведения, путем применения к объекту нескольких декораторов;
  • Разделение монолитного класса, который реализует множество вариантов поведения на более мелкие классы;

При применении этого паттерна возникают следующие сложности:

  • Применение одной конкретной обертки (wrapper) из центра стека (stack);
  • Реализация декоратора, при исключении его зависимости от порядка, в котором обертки уложены в стек.

Паттерн 3: Итератор

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

Чтобы реализовать итератор в Python, у нас есть два возможных варианта:

  1. Реализовать в классе специальные методы __iter__ и __next__.
  2. Использовать генераторы.

Примеры использования

  • Коллекция имеет сложную структуру. Необходимо скрыть ее от клиента из соображений удобства или безопасности;
  • Требуется сократить дублирование обходного кода по всему приложению;
  • Обход элементов различных структур данных;
  • Изначально неизвестны детали структуры данных.

Пример кода

Создание пользовательской коллекции с итератором алфавитного порядка:

         from collections.abc import Iterator, Iterable   class AlphabeticalOrderIterator(Iterator):    _position: int = None    _reverse: bool = False     def __init__(self, collection, reverse=False):        self._collection = sorted(collection)        self._reverse = reverse        self._position = -1 if reverse else 0     def __next__(self):        try:            value = self._collection[self._position]            self._position += -1 if self._reverse else 1        except IndexError:            raise StopIteration()        return value   class WordsCollection(Iterable):    def __init__(self, collection):        self._collection = collection     def __iter__(self):        return AlphabeticalOrderIterator(self._collection)     def get_reverse_iterator(self):        return AlphabeticalOrderIterator(self._collection, True)   if __name__ == "__main__":    wordsCollection = WordsCollection(["Third", "First", "Second"])    print(list(wordsCollection))    print(list(wordsCollection.get_reverse_iterator()))       

Вывод:

         ['First', 'Second', 'Third'] ['Third', 'Second', 'First']     

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

         def prime_generator():   yield 2   primes = [2]   to_check = 3   while True:       sqrt = to_check ** 0.5       is_prime = True       for prime in primes:           if prime > sqrt:               break           if to_check % prime == 0:               is_prime = False               break       if is_prime:           primes.append(to_check)           yield to_check       to_check += 2 generator = prime_generator() print([next(generator) for _ in range(20)])     

Вывод:

         [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]     

Подведем итоги. Возможности итератора:

  • Очистить клиентский код и коллекции, вынеся код обхода в отдельные классы;
  • Реализовать новые типы коллекций и итераторов с передачей их в существующий код без нарушений;
  • Обходить одну и ту же коллекцию с помощью нескольких итераторов параллельно, учитывая, что каждый из них хранит информацию о состоянии итерации;
  • Возможность отложить итерацию и продолжить ее по мере необходимости;

Использование этого паттерна будет лишним, если ваше приложение работает только с простыми коллекциями. Более того, использование итератора может быть менее эффективным, чем прямой обход элементов какой-либо специализированной коллекции.

Заключение

Знание паттернов важно для современного разработчика. Оно помогает решить проблемы внутри вашего кода, используя принципы объектно-ориентированного программирования. Этот материал дает возможность познакомиться с основами работы с паттернами.

***

Хочу освоить больше паттернов, этому где-нибудь учат?

9 февраля стартует курс «Архитектуры и шаблоны проектирования», на котором вы научитесь:

  • строить архитектуры приложений, которые позволяют не снижать скорость разработки по мере развития проекта;
  • писать модульные тесты на Mock-объектах;
  • применять SOLID принципы не только в объектно-ориентированных языках;
  • использовать CI и IoC контейнеры.

Что нужно для старта?

Для старта достаточно знать любой объектно-ориентированный язык программирования: Python, Java, PHP, C++, JavaScript, C# и др.

Интересно, хочу попробовать

  • 1 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close