Какой лучший способ реализации singleton в Python?
У меня есть несколько классов, которые должны стать singleton (мой случай использования - это логгер, но это не важно). Я не хочу загромождать несколько классов лишним кодом, когда я могу просто унаследовать или использовать декоратор.
Лучшие методы:
Метод 1: Декоратор
def singleton(class_):
instances = {}
def getinstance(*args, **kwargs):
if class_ not in instances:
instances[class_] = class_(*args, **kwargs)
return instances[class_]
return getinstance
@singleton
class MyClass(BaseClass):
pass
Плюсы
- Декораторы являются добавочными способом, который часто более интуитивен, чем множественное наследование.
Минусы
- Хотя объекты, созданные с помощью
MyClass(), будут истинными объектами singleton, самMyClassявляется функцией, а не классом, поэтому вы не можете вызывать классовые методы из него. Также для
x = MyClass()
y = MyClass()
t = type(x)()
тогда x == y но x != t && y != t
Метод 2: Базовый класс
class Singleton(object):
_instance = None
def __new__(class_, *args, **kwargs):
if not isinstance(class_._instance, class_):
class_._instance = object.__new__(class_, *args, **kwargs)
return class_._instance
class MyClass(Singleton, BaseClass):
pass
Плюсы
- Это настоящий класс
Минусы
- Множественное наследование - метод new может быть переопределен при наследовании от второго базового класса? Приходится думать больше, чем необходимо.
Метод 3: Метакласс
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
#Python2
class MyClass(BaseClass):
__metaclass__ = Singleton
#Python3
class MyClass(BaseClass, metaclass=Singleton):
pass
Плюсы
- Это настоящий класс
- Автоматически покрывает наследование
- Использует metaclass для своей правильной цели
Минусы
- Есть ли они?
Метод 4: Декоратор, возвращающий класс с тем же именем
def singleton(class_):
class class_w(class_):
_instance = None
def __new__(class_, *args, **kwargs):
if class_w._instance is None:
class_w._instance = super(class_w, class_).__new__(class_, *args, **kwargs)
class_w._instance._sealed = False
return class_w._instance
def __init__(self, *args, **kwargs):
if self._sealed:
return
super(class_w, self).__init__(*args, **kwargs)
self._sealed = True
class_w.__name__ = class_.__name__
return class_w
@singleton
class MyClass(BaseClass):
pass
Плюсы
- Это настоящий класс
- Автоматически покрывает наследование
Минусы
- Нет ли накладных расходов на создание каждого нового класса? Здесь мы создаем два класса для каждого класса, который мы хотим сделать singleton.
- Какова цель атрибута _sealed
- Нельзя вызывать методы с тем же именем в базовых классах с помощью super(), потому что они будут рекурсировать. Это означает, что вы не можете настроить new и не можете подклассировать класс, который требует вызова init вверх по иерархии.
Метод 5: модуль
Файл модуля singleton.py
Плюсы
- Проще лучше, чем сложнее
Минусы
- Не ленивая инициализация
Этот вопрос не для обсуждения того, является ли паттерн singleton желательным, анти-паттерном или для любых религиозных войн, а для обсуждения того, как этот паттерн лучше всего реализовать в Python таким образом, чтобы он был наиболее Pythonic. В данном случае я определяю ‘наиболее Pythonic’ как соответствие принципу ‘наименьшего удивления’.
Реализация паттерна Singleton в Python: метаклассовый подход
Наилучший способ реализации паттерна Singleton в Python, соответствующий принципу наименьшего удивления и являющийся наиболее Pythonic, — это использование подхода на основе метаклассов. Этот метод обеспечивает чистое и надежное решение, которое автоматически обрабатывает наследование, сохраняет истинное поведение класса и дает максимальный контроль без нарушения семантики классов. Метаклассы предлагают идеальный баланс между простотой и мощью, делая их предпочтительным выбором для реализации паттерна Singleton в Python.
Содержание
- Понимание требований паттерна Singleton
- Сравнение методов реализации
- Реализация на основе метаклассов — наиболее Pythonic подход
- Подход с декораторами — просто, но с ограничениями
- Метод базового класса — истинный класс, проблемы с множественным наследованием
- Дополнительные соображения и лучшие практики
- Примеры реального использования
Понимание требований паттерна Singleton
Паттерн Singleton гарантирует, что у класса есть только один экземпляр на протяжении всего приложения и предоставляет глобальную точку доступа к нему. В Python реализация этого паттерна требует тщательного рассмотрения возможностей языка и принципов проектирования.
При реализации Singleton для нескольких классов, таких как система логирования, вам нужно решение, которое:
- Сохраняет истинное поведение класса
- Чисто поддерживает наследование
- Не нарушает семантику классов
- Является потокобезопасным
- Следует принципу “наименьшего удивления” в Python
Как объясняется на refactoring.guru, “Класс Singleton можно реализовать разными способами в Python. Некоторые возможные методы включают: базовый класс, декоратор, метакласс. Мы будем использовать метакласс, потому что он лучше всего подходит для этой цели.”
Сравнение методов реализации
Рассмотрим пять основных подходов к реализации Singleton в Python, их преимущества и ограничения:
| Метод | Потокобезопасность | Поддержка наследования | Поведение класса | Сложность | Pythonic |
|---|---|---|---|---|---|
| Метакласс | ✓ | Отличная | ✓ | Средняя | ★★★★★ |
| Декоратор | ✗ | Плохая | Функция | Низкая | ★★★☆☆ |
| Базовый класс | ✓ (при осторожности) | Ограниченная | ✓ | Средняя | ★★★☆☆ |
| Декоратор класса | ✓ | Хорошая | ✓ | Высокая | ★★☆☆☆ |
| Модуль | ✓ | Н/Д | Модуль | Низкая | ★★★☆☆ |
Исследования последовательно показывают, что метаклассы обеспечивают наиболее надежное и гибкое решение для Singleton в Python. Согласно Leocon.dev, “Метаклассы обеспечивают наиболее надежное и гибкое решение для Singleton в Python” и “Singleton на основе метаклассов часто считается наиболее Pythonic, особенно при работе над крупными приложениями или фреймворками.”
Реализация на основе метаклассов — наиболее Pythonic подход
Подход с метаклассами широко признается наиболее Pythonic способом реализации Singleton. Вот почему он превосходит другие методы:
Пример реализации
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
# Использование в Python 3
class MyClass(BaseClass, metaclass=Singleton):
pass
# Использование в Python 2 (при необходимости)
class MyClass(BaseClass):
__metaclass__ = Singleton
Ключевые преимущества
-
Истинное поведение класса: В отличие от декораторов, метаклассы сохраняют природу классов, позволяя нормально вызывать классовые методы и обращаться к классовым атрибутам.
-
Автоматическая обработка наследования: Как отмечает Programmer Pulse, метаклассы “автоматически покрывают наследование” — любой класс, наследующийся от класса Singleton, автоматически становится Singleton.
-
Правильное использование метаклассов: Метаклассы предназначены для настройки создания классов, что делает эту реализацию семантически правильной и Pythonic.
-
Соображения по потокобезопасности: Хотя базовая реализация не является потокобезопасной, с метаклассами легче добиться потокобезопасности по сравнению с другими подходами.
Версия с потокобезопасностью
Для использования в продакшене рассмотрите добавление потокобезопасности:
import threading
class Singleton(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
Как объясняется на Coderivers.org, “Метод call метакласса вызывается при создании экземпляра класса с этим метаклассом. Если класс отсутствует в словаре _instances, он создает экземпляр с помощью super().call(*args, **kwargs) и сохраняет его.”
Подход с декораторами — просто, но с ограничениями
Метод с декораторами популярен своей простотой, но имеет значительные ограничения:
def singleton(class_):
instances = {}
def getinstance(*args, **kwargs):
if class_ not in instances:
instances[class_] = class_(*args, **kwargs)
return instances[class_]
return getinstance
@singleton
class MyClass(BaseClass):
pass
Ограничения
-
Потеря идентичности класса: Как правильно отметил пользователь,
MyClassстановится функцией, а не классом. Это означает, что вы не можете нормально вызывать классовые методы или обращаться к классовым атрибутам. -
Проблемы с наследованием: Подход с декораторами плохо обрабатывает наследование. Как указывает Leocon.dev, “Декораторы просты, но нарушают наследование и по умолчанию не являются потокобезопасными.”
-
Проблемы с разрешением методов: Пользователь продемонстрировал критическую проблему, при которой
type(x)()создает новый экземпляр, нарушая поведение Singleton.
Хотя SQLpey.com упоминает, что “Элегантный способ реализации паттерна Singleton — использование декоратора. Этот метод прост и соответствует принципу наименьшего удивления, делая его интуитивно понятным для пользователей класса”, практические ограничения перевешивают преимущества простоты для большинства случаев использования.
Метод базового класса — истинный класс, проблемы с множественным наследованием
Подход с базовым классом сохраняет истинное поведение класса, но имеет значительные проблемы с наследованием:
class Singleton(object):
_instance = None
def __new__(class_, *args, **kwargs):
if not isinstance(class_._instance, class_):
class_._instance = object.__new__(class_, *args, **kwargs)
return class_._instance
class MyClass(Singleton, BaseClass):
pass
Ключевые проблемы
-
Сложность множественного наследования: Как правильно идентифицировано, множественное наследование с
__new__может привести к непредсказуемому поведению. Порядок разрешения методов (MRO) может привести к тому, что логика Singleton будет обойдена. -
Общее состояние: Все классы Singleton используют одну и ту же переменную
_instance, что может привести к неожиданному поведению при наличии нескольких классов Singleton. -
Проблемы с инициализацией: Метод
__init__может быть вызван несколько раз, что может быть проблемой для классов, которые должны инициализироваться только один раз.
Как предлагает Coderivers.org, “Простой способ реализации Singleton в Python — использование классовой переменной,” но этот подход имеет значительные ограничения для реальных приложений.
Дополнительные соображения и лучшие практики
Когда использовать Singleton
Хотя вопрос конкретно касается реализации, а не того, следует ли использовать Singleton, стоит отметить, что Singleton уместен для:
- Общих ресурсов, таких как подключения к базе данных
- Управления конфигурацией
- Систем логирования
- Механизмов кэширования
Соображения по производительности
- Накладные расходы метаклассов: Метаклассы имеют небольшую стоимость при определении класса, но это незначительно во время выполнения.
- Использование памяти: Подход с метаклассами хранит экземпляры в словаре, что эффективно для большинства приложений.
- Стоимость инициализации: Singleton должны быть лениво загружаемыми, чтобы избежать ненужной инициализации.
Соображения по тестированию
Singleton могут усложнить тестирование, поскольку они сохраняют состояние между тестами. Рассмотрите:
- Сброс состояния Singleton между тестами
- Внедрение зависимостей для тестируемости
- Использование менеджеров контекста для временных изменений состояния
Как мудро отмечает ThePythonCodingStack.com, “Классический класс Singleton трудно тестировать. Этот вариант упрощает вещи, потому что вы можете присвоить новый Leaderboard game_services.leaderboard или создать отдельный объект GameServices для этой цели.”
Примеры реального использования
Реализация системы логирования
class Logger(metaclass=Singleton):
def __init__(self):
self.logs = []
def log(self, message):
timestamp = datetime.now().isoformat()
self.logs.append(f"{timestamp}: {message}")
print(message)
def get_logs(self):
return self.logs
# Использование
logger1 = Logger()
logger2 = Logger()
print(logger1 is logger2) # True
Пул подключений к базе данных
class DatabaseConnection(metaclass=Singleton):
def __init__(self, connection_string):
self.connection_string = connection_string
self._connection = None
def get_connection(self):
if self._connection is None:
self._connection = create_db_connection(self.connection_string)
return self._connection
Как объясняется на GeeksforGeeks, “Когда каждый объект создает уникальное подключение к базе данных, это сильно повлияет на стоимость и расходы проекта. Поэтому всегда лучше создавать одно подключение, а не создавать лишние нерелевантные подключения, которые могут…”
Заключение
Подход на основе метаклассов выделяется как лучший способ реализации Singleton в Python, особенно когда нужно применить паттерн к нескольким классам. Вот ключевые выводы:
-
Метаклассы — наиболее Pythonic подход — они обеспечивают истинное поведение класса, автоматическую обработку наследования и семантически правильную реализацию.
-
Учитывайте потокобезопасность — добавляйте соответствующие механизмы блокировки для производственных сред для предотвращения состояний гонки при создании экземпляров.
-
Балансируйте простоту и поддерживаемость — хотя метаклассы имеют небольшой кривой обучения, они обеспечивают наиболее поддерживаемое решение для сложных приложений.
-
Избегайте ограничений декораторов — декораторы нарушают семантику классов и наследование, делая их менее подходящими для большинства реализаций Singleton.
-
Осторожно с базовыми классами — множественное наследование с базовыми классами Singleton может привести к непредсказуемому поведению.
Для вашего случая использования с несколькими классами, такими как система логирования, подход с метаклассами даст вам чистый и поддерживаемый код, соответствующий принципу наименьшего удивления в Python. Как заключает статья в Medium, “Паттерн Singleton гарантирует, что у класса есть только один экземпляр и предоставляется глобальная точка доступа к нему” — и реализация на основе метаклассов делает это наиболее элегантно в Python.
Источники
- Методы реализации паттерна Singleton - SQLpey
- Реализация Singleton в Python - XQA Blog
- Создание классов Singleton - ThePythonCodingStack
- Класс Singleton в Python - Orchestra
- Singleton в Python - Programmer Pulse
- Класс Singleton в Python - CodeRivers
- Методы Singleton в Python - Medium
- Лучшие практики Singleton - CodeRivers
- Критические ошибки Singleton - Leocon.dev
- Учебник по паттерну Singleton - Medium