Привет, коллеги!
Когда проект на Python перерастает размеры "скрипта для себя", код неизбежно начинает пухнуть. Появляется путаница, одни и те же проблемы приходится решать по несколько раз, и правка одного модуля ломает три других.
В мире программирования для этого придумали паттерны проектирования. Это не готовый код, а шаблоны мышления — проверенные способы решать типовые задачи так, чтобы код оставался гибким и понятным .
Особенность Python в том, что многие паттерны здесь реализуются проще и элегантнее, чем в строгих Java или C#. Сегодня разберем три самых популярных и полезных паттерна с реальными примерами кода.
Что такое паттерны проектирования (и зачем они нужны)
Представьте, что вы строите дом. Можно лепить стены из глины, а можно использовать типовые решения: фундамент, несущие балки, перекрытия. Паттерны — это такие же стандартизированные "архитектурные решения", только для кода .
Паттерны делятся на три большие группы :
- Порождающие (как создавать объекты)
- Структурные (как строить связи между классами)
- Поведенческие (как объектам общаться между собой)
Мы возьмем по одному хиту из каждой категории.
1. Паттерн Singleton (Одиночка)
Тип: Порождающий.
Суть: Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа .
Где пригодится:
- Подключение к базе данных (чтобы не открывать 100500 соединений)
- Класс с настройками приложения (логично иметь один объект конфига)
- Логгер (пишем всё в один файл из одного места)
Как это работает в Python
В Python есть лайфхак: модули сами по себе являются синглтонами. Но если нужен именно класс — используем метаклассы.
Пример: Менеджер подключения к БД
class SingletonMeta(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 DatabaseConnection(metaclass=SingletonMeta):
"""Наш "единственный" класс для работы с БД"""
def __init__(self):
self.connection = self.connect_to_db()
print("Создаю новое подключение к БД")
def connect_to_db(self):
# Здесь реальная логика подключения
return "Connection object"
# Проверяем
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # Выведет: True (это один и тот же объект!)
print(id(db1), id(db2)) # ID объектов совпадают
При первом вызове создается подключение, при втором — просто возвращается уже существующий объект. Экономия ресурсов налицо.
2. Паттерн Factory (Фабрика)
Тип: Порождающий.
Суть: Создает объекты, не привязывая вызывающий код к конкретным классам. Вместо if-elif-else по всему проекту, мы делегируем создание объектов фабрике .
Где пригодится:
- Приложение должно работать с разными типами уведомлений (email, SMS, push)
- Система платежей (разные платежные шлюзы)
- Парсеры файлов (в зависимости от расширения)
Простая фабрика на практике
Представим, что мы пишем систему уведомлений для интернет-магазина.
from abc import ABC, abstractmethod
# 1. Создаем абстрактный класс (интерфейс) для всех уведомлений
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None:
pass
# 2. Реализуем конкретные виды уведомлений
class EmailNotification(Notification):
def send(self, message: str):
print(f"📧 Отправляем email: {message}")
# Здесь логика отправки email
class SMSNotification(Notification):
def send(self, message: str):
print(f"📱 Отправляем SMS: {message}")
# Здесь логика отправки SMS
class PushNotification(Notification):
def send(self, message: str):
print(f"🔔 Отправляем Push-уведомление: {message}")
# 3. Создаем Фабрику
class NotificationFactory:
@staticmethod
def create_notification(notification_type: str) -> Notification:
"""Фабричный метод, который возвращает нужный объект"""
notifications = {
"email": EmailNotification(),
"sms": SMSNotification(),
"push": PushNotification()
}
notification = notifications.get(notification_type.lower())
if not notification:
raise ValueError(f"Неизвестный тип уведомления: {notification_type}")
return notification
# 4. Используем
if __name__ == "__main__":
# Вызывающий код ничего не знает про классы EmailNotification и т.д.
# Он просто просит фабрику дать ему уведомление по имени
notif = NotificationFactory.create_notification("email")
notif.send("Ваш заказ принят!")
notif2 = NotificationFactory.create_notification("sms")
notif2.send("Курьер выехал")
Что мы выиграли?
Если завтра появится Telegram-уведомление, мы просто добавим класс TelegramNotification и пропишем его в словаре фабрики. Код, который отправляет уведомления (main), вообще не изменится! Это соблюдение принципа открытости/закрытости (OCP).
3. Паттерн Observer (Наблюдатель)
Тип: Поведенческий.
Суть: Создает механизм подписки, позволяющий одним объектам (издателям) следить за изменениями других объектов (подписчиков) . Классика событийно-ориентированного программирования.
Где пригодится:
- Системы мониторинга (датчик температуры обновляет график, логи и включает сигналку)
- Обработка событий в GUI (кнопка нажата — все слушатели узнали)
- Торговые роботы (изменение цены вызывает цепочку действий)
Реализация "издатель-подписчик"
Смоделируем датчик температуры, на который подписаны Логгер и Сигнализация.
import time
import random
# 1. Базовый класс "Издатель" (Subject)
class Subject:
"""Тот, за кем следят (наблюдаемый объект)"""
def __init__(self):
self._observers = [] # Список подписчиков
def attach(self, observer):
"""Подписать наблюдателя"""
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer):
"""Отписать наблюдателя"""
try:
self._observers.remove(observer)
except ValueError:
pass
def notify(self, data):
"""Уведомить всех подписчиков о новых данных"""
for observer in self._observers:
observer.update(data)
# 2. Конкретный издатель (Датчик температуры)
class TemperatureSensor(Subject):
def run_simulation(self):
"""Симуляция работы датчика"""
while True:
temp = random.randint(15, 35) # Эмулируем случайную температуру
print(f"\n[Датчик] Новая температура: {temp}°C")
self.notify(temp) # Оповещаем всех подписчиков!
time.sleep(3) # Новые данные каждые 3 секунды
# 3. Интерфейс для Наблюдателей (не обязателен, но желателен)
class Observer(ABC):
@abstractmethod
def update(self, data):
pass
# 4. Конкретные Наблюдатели (подписчики)
class Logger(Observer):
def update(self, temp):
print(f"[Логгер] Записываем в лог: {temp}°C")
class Alarm(Observer):
def update(self, temp):
if temp > 30:
print(f"[Сигнализация] 🚨 ВНИМАНИЕ! Перегрев: {temp}°C!")
class Display(Observer):
def update(self, temp):
print(f"[Дисплей] Показываем на экране: {temp}°C")
# 5. Запуск
if __name__ == "__main__":
sensor = TemperatureSensor()
# Создаем наблюдателей
logger = Logger()
alarm = Alarm()
display = Display()
# Подписываем их на датчик
sensor.attach(logger)
sensor.attach(alarm)
sensor.attach(display)
# Запускаем мониторинг (в реальном проекте тут был бы основной цикл)
try:
sensor.run_simulation()
except KeyboardInterrupt:
print("\nМониторинг остановлен")
Теперь наш датчик температуры ничего не знает о том, что с данными будут делать. Можно добавить новый класс TelegramBotNotifier, подписать его — и система заработает без изменения кода датчика. Это и есть слабая связанность (low coupling).
Почему это стоит использовать?
- Переиспользование. Вы не изобретаете велосипед. Тысячи умных людей до вас уже придумали, как решать проблему .
- Коммуникация. Сказать "тут мы применили Фабрику" — это сказать очень много. Другой разработчик сразу поймет архитектуру .
- Надежность. Паттерны проверены временем. Они учитывают граничные случаи, о которых вы можете не подумать.
Итоги
Мы разобрали три кита, на которых держится множество приложений:
- Singleton — для уникальных объектов (конфиги, подключения).
- Factory — для гибкого создания семейств объектов.
- Observer — для реактивных систем и обработки событий.
Это база, с которой стоит начать погружение в мир паттернов. А если хотите копнуть вглубь — обратите внимание на книгу Гарри Персиваля "Паттерны разработки на Python", где разбираются более сложные штуки вроде чистой архитектуры и DDD .
А вы используете паттерны в своих проектах? Или считаете, что Python и без них хорош? Делитесь мнением в комментариях — поспорим! 👇