Что такое паттерны проектирования и когда они нужны. Singleton через __new__, метаклассы и декораторы. Monostate (Borg) как Pythonic-альтернатива.
Паттерн — это не решение, которое нужно применить. Это решение, которое нужно понять.
Паттерн проектирования — это повторяемое решение распространённой проблемы проектирования. Это не готовый код, который можно скопировать и вставить, а шаблон, который адаптируется под конкретную задачу.
# ❌ ПЛОХО: Преждевременное применение паттернов
class ConfigFactorySingletonProxy:
"""Комбинация 3 паттернов там, где нужен просто модуль"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# ✅ ХОРОШО: Pythonic-решение
# config.py — модуль уже singleton по природе
database_url = "postgresql://localhost/mydb"
debug = TrueПравило: Используйте паттерн, когда проблема стала очевидной, а не когда вы читаете книгу по паттернам.
Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему.
# ❌ Проблема: несколько экземпляров там, где нужен один
class DatabaseConnection:
def __init__(self):
self.connection = self._connect()
def _connect(self):
print("Создаю новое соединение с БД")
# ... реальное подключение ...
return "connection"
# В разных частях кода создаются новые соединения
db1 = DatabaseConnection() # Создаю новое соединение с БД
db2 = DatabaseConnection() # Создаю новое соединение с БД
# ❌ Два соединения вместо одного — расточительно!class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
class DatabaseConnection(Singleton):
def __init__(self):
# ⚠️ __init__ вызывается КАЖДЫЙ раз при создании!
self.connection = self._connect()
def _connect(self):
print("Создаю соединение")
return "connection"
db1 = DatabaseConnection() # Создаю соединение
db2 = DatabaseConnection() # Создаю соединение (снова!)
print(db1 is db2) # True — экземпляр один
# Но __init__ вызвался дважды! ⚠️Проблема: __init__ вызывается при каждом обращении к конструктору, даже если экземпляр уже существует.
class Singleton:
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not self._initialized:
self.connection = self._connect()
self._initialized = True
def _connect(self):
print("Создаю соединение")
return "connection"
db1 = DatabaseConnection() # Создаю соединение
db2 = DatabaseConnection() # Ничего не происходит
print(db1 is db2) # Trueclass SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabaseConnection(metaclass=SingletonMeta):
def __init__(self):
self.connection = self._connect()
def _connect(self):
return "connection"
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # TrueПреимущество: __init__ вызывается только один раз, потому что __call__ не создаёт новый экземпляр.
from functools import wraps
def singleton(cls):
instances = {}
@wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
def __init__(self):
self.connection = self._connect()
def _connect(self):
return "connection"
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # TrueВажно: db1 и db2 — это не экземпляры DatabaseConnection, а результат вызова get_instance().
# config.py
database_url = "postgresql://localhost/mydb"
debug = True
# main.py
import config
print(config.database_url) # Доступ к состоянию модуля
# other.py
import config
print(config.database_url) # Тот же объект!
# ✅ Модуль в Python — singleton по природе
# import всегда возвращает один и тот же объект из sys.modulesВывод: В Python модуль — это встроенный singleton. Используйте модуль вместо класса-singleton, когда возможно.
Monostate (также известный как Borg) — это вариант singleton, где у класса может быть много экземпляров, но все они разделяют одно и то же состояние.
class Borg:
_shared_state = {}
def __init__(self):
self.__dict__ = self._shared_state
class DatabaseConnection(Borg):
def __init__(self):
super().__init__()
if not hasattr(self, 'connection'):
self.connection = self._connect()
print("Создаю соединение")
def _connect(self):
return "connection"
db1 = DatabaseConnection() # Создаю соединение
db2 = DatabaseConnection() # Ничего не происходит
db3 = DatabaseConnection() # Ничего не происходит
print(db1 is db2) # False — разные экземпляры!
print(db1.connection is db2.connection) # True — общее состояние!
db1.connection = "new_connection"
print(db2.connection) # new_connection — изменение видно во всех!| Аспект | Singleton | Monostate |
|---|---|---|
| Экземпляры | Один | Много |
| Состояние | Одно | Общее |
| Наследование | Сложно | Легко |
| Тестирование | Сложно (глобал) | Легче (можно сбросить) |
| Pythonic | Нет | Более Pythonic |
class Config(Borg):
_shared_state = {}
def __init__(self):
super().__init__()
if not hasattr(self, 'debug'):
self.debug = False
self.database_url = "prod://db"
# В тестах можно легко сбросить состояние
def test_something():
Config._shared_state = {} # Сброс!
config = Config()
assert config.debug == False# ❌ ПЛОХО: Singleton как глобальная переменная
class Logger(Singleton):
def log(self, msg):
print(msg)
# Везде в коде:
Logger().log("message") # Скрытая зависимость!Проблема: Невозможно подменить логгер в тестах, скрытая зависимость.
# ✅ ХОРОШО: Явная зависимость
class Service:
def __init__(self, logger):
self.logger = logger
def do_work(self):
self.logger.log("working")
# В production:
service = Service(Logger())
# В тестах:
class MockLogger:
def log(self, msg): pass
service = Service(MockLogger()) # Легко тестировать!# ❌ ПЛОХО: Singleton делает слишком много
class AppManager(Singleton):
def __init__(self):
self.db = Database()
self.cache = Cache()
self.logger = Logger()
self.config = Config()
# ... ещё 20 зависимостей ...Решение: Разделите на отдельные компоненты с явными зависимостями.
class Cache(Borg):
_shared_state = {}
def __init__(self):
super().__init__()
if not hasattr(self, '_data'):
self._data = {}
def get(self, key):
return self._data.get(key)
def set(self, key, value):
self._data[key] = value
# Использование
cache1 = Cache()
cache1.set("user:1", {"name": "Alice"})
cache2 = Cache()
print(cache2.get("user:1")) # {"name": "Alice"} — общие данные!# logger.py
import logging
logger = logging.getLogger("myapp")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
logger.addHandler(handler)
# main.py
from logger import logger
logger.info("Application started")| Паттерн | Когда использовать | Pythonic-альтернатива |
|---|---|---|
| Singleton | Нужен строго один экземпляр | Модуль |
| Monostate | Нужно общее состояние, но важна тестируемость | — |
| Модуль | Глобальное состояние, конфигурация | — |
Главный принцип: Используйте простейшее решение. Модуль > Monostate > Singleton.
Изучите тему Factory Method и Abstract Factory для создания объектов без привязки к конкретным классам.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.