Użyj metaklasy
Poleciłbym Metodę 2 , ale lepiej jest użyć metaklasy niż klasy podstawowej. Oto przykładowa implementacja:
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
Lub w Python3
class Logger(metaclass=Singleton):
pass
Jeśli chcesz uruchomić za __init__
każdym razem, gdy klasa jest wywoływana, dodaj
else:
cls._instances[cls].__init__(*args, **kwargs)
do if
oświadczenia wSingleton.__call__
.
Kilka słów o metaklasach. Metaklasa jest klasą klasy ; to znaczy klasa jest instancją jej metaklasy . Metaklasę obiektu można znaleźć w Pythonie za pomocą type(obj)
. Normalne klasy nowego stylu są typu type
. Logger
w powyższym kodzie będzie typu class 'your_module.Singleton'
, podobnie jak (jedyna) instancja Logger
będzie typu class 'your_module.Logger'
. Podczas rozmowy telefonicznej z rejestratora Logger()
, Python najpierw pyta o metaklasą Logger
, Singleton
co robić, umożliwiając tworzenie instancji należy uprzedzać. Ten proces jest taki sam, jak Python pytający klasę, co robić, wywołując, __getattr__
gdy odwołujesz się do jednego z jego atrybutówmyclass.attribute
.
Metaklasa zasadniczo decyduje, co oznacza definicja klasy i jak ją wdrożyć. Zobacz na przykład http://code.activestate.com/recipes/498149/ , który zasadniczo odtwarza style C struct
w Pythonie przy użyciu metaklas. Wątek Jakie są (konkretne) przypadki użycia metaklas? podaje także kilka przykładów, na ogół wydają się one być związane z programowaniem deklaratywnym, zwłaszcza w przypadku ORM.
W tej sytuacji, jeśli użyjesz metody nr 2 , a podklasa definiuje __new__
metodę, będzie ona wykonywana przy każdym wywołaniu SubClassOfSingleton()
- ponieważ odpowiada za wywołanie metody zwracającej przechowywaną instancję. Metaklasa zostanie wywołana tylko raz , gdy zostanie utworzona jedyna instancja. Chcesz dostosować, co to znaczy nazywać klasę , zależnie od jej typu.
Zasadniczo sensowne jest użycie metaklasy do wdrożenia singletonu. Singleton jest wyjątkowy, ponieważ jest tworzony tylko raz , a metaklasa to sposób, w jaki dostosowujesz tworzenie klasy . Korzystanie z metaklasy daje większą kontrolę na wypadek konieczności dostosowania definicji klas singletonów na inne sposoby.
Twoje singletony nie będą wymagały wielokrotnego dziedziczenia (ponieważ metaklasa nie jest klasą podstawową), ale w przypadku podklas utworzonej klasy, które używają wielokrotnego dziedziczenia, musisz upewnić się, że klasa singleton jest pierwszą / najbardziej lewą klasą z metaklasą, która redefiniuje __call__
Jest to bardzo mało prawdopodobne, aby stanowił problem. Dict instancji nie znajduje się w przestrzeni nazw instancji, więc przypadkowo go nie zastąpi.
Usłyszysz również, że wzorzec singletonu narusza „zasadę pojedynczej odpowiedzialności” - każda klasa powinna zrobić tylko jedną rzecz . W ten sposób nie musisz się martwić, że zepsujesz jedną rzecz, którą robi kod, jeśli musisz zmienić inną, ponieważ są one osobne i hermetyzowane. Implementacja metaklasy przechodzi ten test . Metaklasa jest odpowiedzialna za wymuszanie wzorca, a utworzona klasa i podklasy nie muszą mieć świadomości, że są singletonami . Metoda nr 1 kończy się niepowodzeniem tego testu, jak zauważyłeś w „MyClass sama jest funkcją, a nie klasą, więc nie można wywoływać z niej metod klas”.
Wersja kompatybilna z Python 2 i 3
Pisanie czegoś, co działa zarówno w Python2, jak i 3, wymaga użycia nieco bardziej skomplikowanego schematu. Ponieważ metaklasy są zwykle podklasami typu type
, możliwe jest użycie jednej z nich, aby dynamicznie utworzyć pośrednią klasę bazową w czasie wykonywania z nią jako metaklasą, a następnie użyć jej jako Singleton
klasy podstawowej publicznej klasy bazowej. Trudniej to wytłumaczyć niż zrobić, jak pokazano poniżej:
# 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
Ironicznym aspektem tego podejścia jest to, że wykorzystuje podklasę do implementacji metaklasy. Jedną z możliwych zalet jest to, że w przeciwieństwie do czystej metaklasy isinstance(inst, Singleton)
powróci True
.
Korekty
W innym temacie zapewne już to zauważyłeś, ale implementacja klasy podstawowej w oryginalnym poście jest nieprawidłowa. _instances
należy odwoływać się do klasy , należy jej użyć super()
lub rekursuje się i __new__
jest to metoda statyczna, do której należy przekazać klasę , a nie metoda klasy, ponieważ rzeczywista klasa nie została jeszcze utworzona nazywa się. Wszystkie te rzeczy będą prawdziwe także dla implementacji metaklasy.
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()
Dekorator powracający klasę
Początkowo pisałem komentarz, ale był za długi, więc dodam go tutaj. Metoda nr 4 jest lepsza niż inna wersja dekoratora, ale zawiera więcej kodu niż jest to potrzebne do singletona i nie jest tak jasne, co robi.
Główne problemy wynikają z tego, że klasa jest własną klasą podstawową. Po pierwsze, czy to nie dziwne, że klasa jest podklasą niemal identycznej klasy o tej samej nazwie, która istnieje tylko w jej __class__
atrybucie? Oznacza to również, że nie można określić żadnych metod, które wywołać metodę o tej samej nazwie na ich klasy podstawowej z super()
ponieważ będą one recurse. Oznacza to, że twoja klasa nie może się dostosowywać __new__
i nie może wywodzić się z żadnych klas, które trzeba __init__
do nich wezwać.
Kiedy stosować wzór singletonu
Twój przypadek użycia jest jednym z lepszych przykładów chęci skorzystania z singletonu. Mówisz w jednym z komentarzy: „Dla mnie logowanie zawsze wydawało się naturalnym kandydatem do Singletonów”. Masz absolutną rację .
Kiedy ludzie mówią, że singletony są złe, najczęstszym powodem jest ukryty stan wspólny . Podczas gdy w przypadku zmiennych globalnych i importu modułów najwyższego poziomu jawny jest stan współdzielony, inne przekazywane obiekty są generalnie tworzone. To dobra uwaga, z dwoma wyjątkami .
Pierwszym i wspomnianym w różnych miejscach jest to, że singletony są stałe . Używanie stałych globalnych, zwłaszcza wyliczeń, jest powszechnie akceptowane i uważane za rozsądne, ponieważ bez względu na wszystko, żaden z użytkowników nie może ich zepsuć dla żadnego innego użytkownika . Dotyczy to również stałego singletonu.
Drugi wyjątek, o którym mniej się wspomina, jest odwrotny - gdy singleton jest tylko ujściem danych , a nie źródłem danych (bezpośrednio lub pośrednio). Właśnie dlatego rejestratorzy czują się jak „naturalne” zastosowanie singletonów. Ponieważ różni użytkownicy nie zmieniają rejestratorów w sposób, w jaki dbają o nich inni użytkownicy, nie ma tak naprawdę wspólnego stanu . To neguje główny argument przeciwko wzorowi singletonów i czyni je rozsądnym wyborem ze względu na łatwość użycia do zadania.
Oto cytat z http://googletesting.blogspot.com/2008/08/root-cause-of-singletons.html :
Jest jeden rodzaj Singletona, który jest OK. Jest to singleton, w którym wszystkie osiągalne obiekty są niezmienne. Jeśli wszystkie obiekty są niezmienne, Singleton nie ma stanu globalnego, ponieważ wszystko jest stałe. Ale tak łatwo jest zmienić ten singleton w zmienny, że jest bardzo śliski. Dlatego też jestem przeciwny tym Singletonom, nie dlatego, że są źli, ale dlatego, że bardzo łatwo im się popsuć. (Na marginesie, wyliczenia Java są tylko tego rodzaju singletonami. Dopóki nie wprowadzisz stanu do swojego wyliczenia, wszystko jest w porządku, więc proszę nie.)
Innym rodzajem Singletonów, które są częściowo akceptowalne, są te, które nie wpływają na wykonanie kodu, nie mają „efektów ubocznych”. Logowanie jest doskonałym przykładem. Jest załadowany Singletonami i stanem globalnym. Jest to do przyjęcia (ponieważ nie zaszkodzi ci to), ponieważ twoja aplikacja nie zachowuje się inaczej, niezależnie od tego, czy dany rejestrator jest włączony, czy nie. Informacje płyną tutaj w jedną stronę: z aplikacji do rejestratora. Nawet jeśli rejestratory są stanem globalnym, ponieważ żadne informacje nie przepływają z rejestratorów do aplikacji, rejestratory są dopuszczalne. Powinieneś nadal wstrzykiwać program rejestrujący, jeśli chcesz, aby test potwierdził, że coś się loguje, ale ogólnie Loggery nie są szkodliwe, mimo że są pełne stanu.
foo.x
lub jeśli nalegaszFoo.x
zamiastFoo().x
); użyj atrybutów klas i metod static / class (Foo.x
).