Whether singletons are "bad" or not seems to be a matter of taste. Certainly they have their place, and any of the variations on the singleton theme should work for you.
The "Borg pattern" (less colorfully & much less commonly, "StatelessProxy" or "Monostate") has been a popular Python alternative to Singleton probably ever since Alex Martelli's clever ActiveState recipe Singleton? We don't need no stinkin' singleton: the Borg design pattern. It differs from Singleton in allowing multiple distinct objects of a class, all sharing common data. The Singleton pattern, by contrast, ensures that only one instance of a class is ever created.
A discussion of the Borg vs Singleton issue can be found in this stackoverflow post: Why is the Borg pattern better than the Singleton pattern in Python. The implementations at the top of the post might be puzzling due to the missing _init_default_register
method, whose purpose is to create and initialize the common data attributes, once only. For reference and comparison, here are complete implementations (in Python 3), both of which create a single data attribute (a dict named data
):
The standard way to implement Singleton in Python is with a metaclass;
class Singleton(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
def __call__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
You would make your statistics-tracking class a singleton by giving it this metaclass:
class StatsTrackerSingleton(metaclass=Singleton):
def __init__(self):
self.data = {}
# ... methods to update data, summarize it, etc. ...
The Borg pattern is simpler to implement and, because it doesn't use a metaclass, potentially more flexible:
class StatsTrackerBorg():
__shared_data = {'data':{}}
# RHS guarantees a common dict named `data`
def __init__(self):
"""Make every instance use StatsTrackerBorg.__shared_data
for its attribute dict."""
self.__dict__ = self.__shared_data
# ... methods to update data, summarize it, etc. ...
With both patterns, as implemented above, you can use the common dict data
, and you can simply get and set shared attributes using the dot operator. For example, using the Borg class:
>>> a = StatsTrackerBorg()
>>> b = StatsTrackerBorg()
>>> a is b # would be True for StatsTrackerSingleton
False
>>> vars(a) is vars(b)
True
>>> vars(a)
{'data': {}}
>>> a.data['running_time'] = 10000
>>> b.bar = 10
>>> vars(a)
{'data': {'running_time': 10000}, 'bar': 10}
>>> b.foo = 'y'
>>> a.foo
'y'
A noteworthy difference between the two patterns: a subclass of a "Borg'ed" class shares the same common state with the superclass, and can still add more shared state accessible to its instances, whereas each subclass of a Singleton class gets its own unique instance and thus its own common state, disjoint from that of the superclass. For some intended applications, one of these behaviors may be clearly more appropriate than the other.