Singleton
A singleton is a object, usually stored statically, that is instantiated on the first request and then returns that instance on all further requests. In python, the cleanest way to do this is with a decorator.
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 Database:
def __init__(self):
print('Loading database')
if __name__ == '__main__':
d1 = Database()
d2 = Database()
print(d1 == d2)
Monostate
A monostate is a type of singleton where all instances of a class share a set of shared state, but are still different objects with other member attributes.
class CEO:
__shared_state = {
'name': 'Steve',
'age': 55
}
def __init__(self):
self.__dict__ = self.__shared_state
def __str__(self):
return f'{self.name} is {self.age} years old'
if __name__ == '__main__':
ceo1 = CEO()
print(ceo1)
ceo1.age = 66
ceo2 = CEO()
ceo2.age = 77
print(ceo1)
print(ceo2)
Testing
It can be difficult to test singletons because they are not usually injected into a class by design. Every client that initializes the class will receive the same object so there's no use to pass it around. The correct way to test these then are to instead pass the singleton class into the initializer with the default being instantiating your production class. This allows you to inject a mock singleton class for testing.
import unittest
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 Database(metaclass=Singleton):
def __init__(self):
self.population = {}
f = open('capitals.txt', 'r')
lines = f.readlines()
for i in range(0, len(lines), 2):
self.population[lines[i].strip()] = int(lines[i + 1].strip())
f.close()
# Accesses production class directory
class SingletonRecordFinder:
def total_population(self, cities):
result = 0
for c in cities:
result += Database().population[c]
return result
# Takes an argument with default of prod class
class ConfigurableRecordFinder:
def __init__(self, db=Database()):
self.db = db
def total_population(self, cities):
result = 0
for c in cities:
result += self.db.population[c]
return result
class DummyDatabase:
population = {
'alpha': 1,
'beta': 2,
'gamma': 3
}
def get_population(self, name):
return self.population[name]
class SingletonTests(unittest.TestCase):
def test_is_singleton(self):
db = Database()
db2 = Database()
self.assertEqual(db, db2)
def test_singleton_total_population(self):
""" This tests on a live database :( """
rf = SingletonRecordFinder()
names = ['Seoul', 'Mexico City']
tp = rf.total_population(names)
self.assertEqual(tp, 17500000 + 17400000) # what if these change?
ddb = DummyDatabase()
def test_dependent_total_population(self):
crf = ConfigurableRecordFinder(self.ddb)
self.assertEqual(
crf.total_population(['alpha', 'beta']),
3
)