Використовуйте метаклас
Я б рекомендував метод №2 , але вам краще використовувати метаклас, ніж базовий клас. Ось приклад реалізації:
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]
class Logger(object):
__metaclass__ = Singleton
Або в Python3
class Logger(metaclass=Singleton):
pass
Якщо ви хочете запускати __init__
кожен раз, коли викликається клас, додайте
else:
cls._instances[cls].__init__(*args, **kwargs)
до if
заяви в Singleton.__call__
.
Кілька слів про метакласи. Метаклас - це клас класу ; тобто клас - це примірник його метакласу . Ви знаходите метаклас об'єкта в Python за допомогою type(obj)
. Звичайні класи нового стилю мають тип type
. Logger
у наведеному вище коді буде тип class 'your_module.Singleton'
, як і (лише) екземпляр Logger
типу class 'your_module.Logger'
. При виклику логгер з Logger()
, Python спочатку запитує метаклассом Logger
, Singleton
що ж робити, що дозволяє створення екземпляра бути попередньо спорожнити. Цей процес такий же, як і Python, запитуючи клас, що робити, зателефонувавши, __getattr__
коли ви посилаєтесь на один з його атрибутів, виконуючи завдання myclass.attribute
.
Метаклас по суті визначає, що означає визначення класу і як реалізувати це визначення. Дивіться, наприклад, http://code.activestate.com/recipes/498149/ , який по суті відтворює C-стилі struct
в Python, використовуючи метакласи. Нитка Які деякі (конкретні) випадки використання для метакласів? також наводить деякі приклади, вони, як правило, пов'язані з декларативним програмуванням, особливо, як це використовується в ORM.
У цій ситуації, якщо ви використовуєте ваш метод №2 , а підклас визначає __new__
метод, він буде виконуватися щоразу, коли ви викликаєте SubClassOfSingleton()
- тому що він відповідає за виклик методу, який повертає збережений екземпляр. З метакласом він буде викликаний лише один раз , коли буде створений єдиний екземпляр. Ви хочете налаштувати, що означає називати клас , який визначається його типом.
Взагалі, для реалізації синглтона має сенс використовувати метаклас. Синглтон особливий тим, що створюється лише один раз , а метаклас - це спосіб налаштувати створення класу . Використання метакласу дає вам більше контролю у випадку, якщо вам потрібно налаштувати визначення однокласного класу іншими способами.
Вашим одиночним клавішам не буде потрібно багатократне успадкування (оскільки метаклас не є базовим класом), але для підкласів створеного класу, які використовують множинне успадкування, вам потрібно переконатися, що клас однотонних є першим / лівим лівим з метакласом, який визначає повторно __call__
Це навряд чи буде проблемою. Дікт екземпляра відсутній у просторі імен екземпляра, тому його випадково не перезаписати.
Ви також почуєте, що одинарний зразок порушує "Принцип єдиної відповідальності" - кожен клас повинен робити лише одне . Таким чином, вам не доведеться турбуватися про те, щоб зіпсувати одне, що робить код, якщо вам потрібно змінити інше, оскільки вони є окремими та інкапсульованими. Реалізація метакласу проходить цей тест . Метаклас відповідає за виконання шаблону, і створений клас та підкласи не повинні знати про те, що вони є однотонними . Метод №1 провалює цей тест, як ви зазначили з "MyClass сам по собі є функцією aa, а не класом, тому ви не можете викликати методи класу з нього."
Сумісна версія Python 2 та 3
Для написання чогось, що працює як в Python2, так і в 3, потрібно використовувати трохи складнішу схему. Оскільки метакласи, як правило, є підкласами типу type
, можна використовувати один для динамічного створення посередницького базового класу під час виконання з ним як свого метакласу, а потім використовувати його як Singleton
базовий клас публічного базового класу. Це важче пояснити, ніж зробити, як показано далі:
# works in Python 2 & 3
class _Singleton(type):
""" A metaclass that creates a Singleton base class when called. """
_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]
class Singleton(_Singleton('SingletonMeta', (object,), {})): pass
class Logger(Singleton):
pass
Іронічний аспект цього підходу полягає в тому, що він використовує підкласи для реалізації метакласу. Однією з можливих переваг є те, що, на відміну від чистого метакласу, isinstance(inst, Singleton)
повернеться True
.
Виправлення
З іншої теми ви, напевно, вже помітили це, але реалізація базового класу в початковій публікації неправильна. _instances
потрібно посилатися на клас , вам потрібно використовувати super()
або ви повторюєте , і __new__
це фактично статичний метод, до якого вам потрібно передати клас , а не метод класу, оскільки власне клас ще не створений, коли він це називається. Все це буде вірно і для реалізації метакласу.
class Singleton(object):
_instances = {}
def __new__(class_, *args, **kwargs):
if class_ not in class_._instances:
class_._instances[class_] = super(Singleton, class_).__new__(class_, *args, **kwargs)
return class_._instances[class_]
class MyClass(Singleton):
pass
c = MyClass()
Декоратор, що повертається до класу
Я спочатку писав коментар, але це було занадто довго, тому я додам це тут. Метод №4 кращий за іншу версію декоратора, але це більше коду, ніж потрібно одиночному, і не так зрозуміло, що він робить.
Основні проблеми випливають із класу - це власний базовий клас. По-перше, хіба не дивно, щоб клас був підкласом майже однакового класу з тим самим іменем, який існує лише в його __class__
атрибуті? Це також означає , що ви не можете визначити , які методи , які викликають метод з тим же ім'ям на їх базовий клас з , super()
тому що вони будуть рекурсії. Це означає, що ваш клас не може налаштувати __new__
і не може виходити з будь-яких класів, які потребують __init__
їх використання.
Коли використовувати одинарний візерунок
Ваш випадок використання - один з кращих прикладів бажань використовувати одиночний. Ви говорите в одному з коментарів: "Мені реєстрація завжди здавалася природним кандидатом для одиноких". Ви абсолютно праві .
Коли люди кажуть, що одиночні особи погані, найпоширенішою причиною є те, що це неявний загальний стан . У той час як глобальні змінні та імпорт модулів вищого рівня є явним загальним станом, інші об'єкти, які передаються навколо, як правило, інстанціюються. Це хороший момент, за двома винятками .
Перше і те, що згадується в різних місцях, це коли одинаки постійні . Використання глобальних констант, особливо переліків, є загальноприйнятим і вважається розумним, тому що, незважаючи ні на що, жоден з користувачів не може зіпсувати їх іншим користувачам . Це однаково справедливо для постійного сингтона.
Другий виняток, про який менше згадують, - навпаки - коли сингтон - це лише раковина даних , а не джерело даних (прямо чи опосередковано). Ось чому лісоруби відчувають себе як "природне" використання для одиночних. Оскільки різні користувачі не змінюють реєстратори способами, якими будуть піклуватися інші користувачі, стан справді не є спільним . Це заперечує основний аргумент проти однотонної картини і робить їх розумним вибором через їх простоту використання для виконання завдання.
Ось цитата з http://googletesting.blogspot.com/2008/08/root- why-of-singletons.html :
Тепер є один вид Сінглтона, який добре. Це синглтон, де всі доступні об'єкти незмінні. Якщо всі об'єкти незмінні, то Сінглтон не має глобального стану, оскільки все є постійним. Але так легко перетворити цей вид одинарного в перемінний, це дуже слизький схил. Тому я і проти цих синглтонів, не тому, що вони погані, а тому, що їм дуже просто погано. (Як бічна примітка, перерахування Java - це лише такі одинаки. Поки ви не введете стан у свій перелік, ви все в порядку, тому, будь ласка, не робіть це.)
Інший вид Сінглтонів, які є напівприйнятними, - це ті, які не впливають на виконання вашого коду. Вони не мають "побічних ефектів". Ідеальний приклад - ведення журналів. Він завантажений Singletons та глобальним станом. Це прийнятно (так як це не зашкодить вам), оскільки ваша програма не поводиться нічим, незалежно від того, включений чи ні той реєстратор. Інформація тут протікає одним способом: із вашої програми до реєстратора. Навіть думки реєстраторів є глобальним станом, оскільки від реєстраторів до вашої програми не надходить інформація, реєстратори є прийнятними. Ви все одно повинні вводити свій реєстратор, якщо ви хочете, щоб ваш тест стверджував, що щось зареєстровано, але в цілому Logger не є шкідливим, незважаючи на те, що він повний.
foo.x
або якщо ви наполягаєтеFoo.x
замість ньогоFoo().x
); використовувати атрибути класу та статичні / класичні методи (Foo.x
).