The Singleton Pattern
Learn about the Singleton design pattern in Python.
We'll cover the following
Overview
The Singleton pattern is a source of some controversy; many have accused it of being an anti-pattern, a pattern that should be avoided, not promoted. In Python, if someone is using the Singleton pattern, they’re almost certainly doing something wrong, probably because they’re coming from a more restrictive programming language.
So, why discuss it at all? Singleton is useful in overly object-oriented languages and is a vital part of traditional object-oriented programming. More relevantly, the idea behind singleton is useful, even if we implement the concept in a totally different way in Python.
The basic idea behind the Singleton pattern is to allow exactly one instance of a certain object to exist. Typically, this object is a sort of manager class. Such manager objects often need to be referenced by a wide variety of other objects; passing references to the manager object around to the methods and constructors that need them can make code hard to read.
Instead, when a singleton is used, the separate objects request the single instance of the manager object from the class. The UML diagram doesn’t fully describe it, but here it is for completeness:
In most programming environments, singletons are enforced by making the constructor private (so no one can create additional instances of it), and then providing a static method to retrieve the single instance. This method creates a new instance the first time it is called, and then returns that same instance for all subsequent calls.
Singleton implementation
Python doesn’t have private constructors, but for this purpose, we can use the __new__()
class method to ensure that only one instance is ever created:
class OneOnly:_singleton = Nonedef __new__(cls, *args, **kwargs):if not cls._singleton:cls._singleton = super().__new__(cls, *args, **kwargs)return cls._singleton
When __new__()
is called, it normally constructs a new instance of the requested class. When we override it, we first check whether our singleton instance has been created; if not, we create it using a super
call. Thus, whenever we call the constructor on OneOnly
, we always get the exact same instance:
class OneOnly:_singleton = Nonedef __new__(cls, *args, **kwargs):if not cls._singleton:cls._singleton = super().__new__(cls, *args, **kwargs)return cls._singletono1 = OneOnly()o2 = OneOnly()print(o1 == o2)print(id(o1) == id(o2))print(o1)print(o2)
The two objects are equal and located at the same address; thus, they are the same object. This particular implementation isn’t very transparent, since it’s not obvious that the special method is being used to create a singleton object.
We don’t actually need this. Python provides two built-in Singleton patterns we can leverage. Rather than invent something hard to read, there are two choices:
-
A Python module is a singleton. One
import
will create a module. All subsequent attempts to import the module return the one-and-only singleton instance of the module. In places where an application-wide configuration file or cache is required, make this part of a distinct module. Library modules likelogging
,random
, and evenre
have module-level singleton caches. We’ll look at using module-level variables below. -
A Python class definition can also be pressed into service as a singleton. A class can only be created once in a given namespace. Consider using a class with class-level attributes as a singleton object. This means defining methods with the
@staticmethod
decorator because there will never be an instance created, and there’s noself
variable.
To use module-level variables instead of a complex Singleton pattern, we instantiate the class after we’ve defined it. We can improve our State pattern implementation from earlier on to use singleton objects for each of the states. Instead of creating a new object every time we change states, we can create a collection of module-level variables that are always accessible.
We’ll make a small but very important design change, also. In the examples above, each state has a reference to the Message
object that is being accumulated. This required us to provide the Message
object as part of constructing a new NMEA_State
object; we used code like return Body(self.message)
to switch to a new state, Body
, while working on the same Message
instance.
If we don’t want to create (and recreate) state objects, we need to provide the Message
as an argument to the relevant methods.
Here’s the revised NMEA_State
class:
class NMEA_State:def enter(self, message: "Message") -> "NMEA_State":return selfdef feed_byte(self,message: "Message",input: int) -> "NMEA_State":return selfdef valid(self, message: "Message") -> bool:return Falsedef __repr__(self) -> str:return f"{self.__class__.__name__}()"
This variant on the NMEA_State
class doesn’t have any instance variables. All the methods work with argument values passed in by a client. Here are the individual state definitions:
class NMEA_State:def __init__(self, message: "Message") -> None:self.message = messagedef feed_byte(self, input: int) -> "NMEA_State":return selfdef valid(self) -> bool:return Falsedef __repr__(self) -> str:return f"{self.__class__.__name__}({self.message})"class Waiting(NMEA_State):def feed_byte(self,message: "Message",input: int) -> "NMEA_State":return selfif input == ord(b"$"):return HEADERreturn selfclass Header(NMEA_State):def enter(self, message: "Message") -> "NMEA_State":message.reset()return selfdef feed_byte(self,message: "Message",input: int) -> "NMEA_State":return selfif input == ord(b"$"):return HEADERsize = message.body_append(input)if size == 5:return BODYreturn selfclass Body(NMEA_State):def feed_byte(self,message: "Message",input: int) -> "NMEA_State":return selfif input == ord(b"$"):return HEADERif input == ord(b"*"):return CHECKSUMsize = message.body_append(input)return selfclass Checksum(NMEA_State):def feed_byte(self,message: "Message",input: int) -> "NMEA_State":return selfif input == ord(b"$"):return HEADERif input in {ord(b"\n"), ord(b"\r")}:# Incomplete checksum... Will be invalid.return ENDsize = message.checksum_append(input)if size == 2:return ENDreturn selfclass End(NMEA_State):def feed_byte(self,message: "Message",input: int) -> "NMEA_State":return selfif input == ord(b"$"):return HEADERelif input not in {ord(b"\n"), ord(b"\r")}:return WAITINGreturn selfdef valid(self, message: "Message") -> bool:return message.valid
Here are the module-level variables created from instances of each of these NMEA_State
classes.
WAITING = Waiting()HEADER = Header()BODY = Body()CHECKSUM = Checksum()END = End()
Within each of these classes, we can refer to these five global variables to change parsing state. The ability to refer to a global that’s defined after the class can seem
a little mysterious at first. It works out beautifully because Python variable names are not resolved to objects until runtime. When each class is being built, a name like CHECKSUM
is little more than a string of letters. When evaluating the Body.feed_byte()
method and it’s time to return the value of CHECKSUM
, then the name is resolved to the singleton instance of the Checksum()
class:
Note how the Header
class was refactored. In the version where each state has an __init__()
, we could explicitly evaluate Message.reset()
when entering the Header
state. Since we’re not creating new state objects in this design, we need a way to handle the special case of entering a new state, and performing an enter()
method one time only to do initialization or setup. This requirement leads to a small change in the Reader
class:
from __future__ import annotationsfrom typing import Optional, Iterable, Iterator, castclass NMEA_State:def __init__(self, message: "Message") -> None:self.message = messagedef feed_byte(self, input: int) -> "NMEA_State":return selfdef valid(self) -> bool:return Falsedef __repr__(self) -> str:return f"{self.__class__.__name__}({self.message})"class Waiting(NMEA_State):def feed_byte(self, input: int) -> NMEA_State:if input == ord(b"$"):return Header(self.message)return selfclass Header(NMEA_State):def __init__(self, message: "Message") -> None:self.message = messageself.message.reset()def feed_byte(self, input: int) -> NMEA_State:if input == ord(b"$"):# Reset any accumulated bytesreturn Header(self.message)size = self.message.body_append(input)if size == 5:return Body(self.message)return selfclass Body(NMEA_State):def feed_byte(self, input: int) -> NMEA_State:if input == ord(b"$"):return Header(self.message)if input == ord(b"*"):return Checksum(self.message)self.message.body_append(input)return selfclass Checksum(NMEA_State):def feed_byte(self, input: int) -> NMEA_State:if input == ord(b"$"):return Header(self.message)if input in {ord(b"\n"), ord(b"\r")}:return End(self.message)size = self.message.checksum_append(input)if size == 2:return End(self.message)return selfclass End(NMEA_State):def feed_byte(self, input: int) -> NMEA_State:if input == ord(b"$"):return Header(self.message)elif input not in {ord(b"\n"), ord(b"\r")}:return Waiting(self.message)return selfdef valid(self) -> bool:return self.message.validclass Message:def __init__(self) -> None:self.body = bytearray(80)self.checksum_source = bytearray(2)self.body_len = 0self.checksum_len = 0self.checksum_computed = 0def reset(self) -> None:self.body_len = 0self.checksum_len = 0self.checksum_computed = 0def body_append(self, input: int) -> int:self.body[self.body_len] = inputself.body_len += 1self.checksum_computed ^= inputreturn self.body_lendef checksum_append(self, input: int) -> int:self.checksum_source[self.checksum_len] = inputself.checksum_len += 1return self.checksum_len@propertydef valid(self) -> bool:return (self.checksum_len == 2and int(self.checksum_source, 16) == self.checksum_computed)def header(self) -> bytes:return bytes(self.body[:5])def fields(self) -> list[bytes]:return bytes(self.body[: self.body_len]).split(b",")def __repr__(self) -> str:body = self.body[: self.body_len]checksum = self.checksum_source[: self.checksum_len]return f"Message({body}, {checksum}, computed={self.checksum_computed:02x})"def message(self) -> bytes:return (b"$"+ bytes(self.body[: self.body_len])+ b"*"+ bytes(self.checksum_source[: self.checksum_len]))class Reader:def __init__(self) -> None:self.buffer = Message()self.state: NMEA_State = Waiting(self.buffer)def read(self, source: Iterable[bytes]) -> Iterator[Message]:for byte in source:self.state = self.state.feed_byte(cast(int, byte))if self.buffer.valid:yield self.bufferself.buffer = Message()self.state = Waiting(self.buffer)message = b'''$GPGGA,161229.487,3723.2475,N,12158.3416,W,1,07,1.0,9.0,M,,,,0000*18$GPGLL,3723.2475,N,12158.3416,W,161229.487,A,A*41'''rdr = Reader()result = list(rdr.read(message))print(result)
We don’t trivially replace the value of the self.state
instance variable with the result of the self.state.feed_byte()
evaluation. Instead, we compare the previous value of self.state
with the next value, new_state
, to see if there was a state change. If there was a change, then we need to evaluate enter()
on the new state, to allow the state change to do any required one-time initialization.
In this example we aren’t wasting memory creating a bunch of new instances of each state object that must later be garbage collected. Instead, we are reusing a single state object for each piece of the incoming data stream. Even if multiple parsers are running at once, only these state objects need to be used. The stateful message data is kept separate from the state processing rules in each state object.
Note: We’ve combined two patterns, each with different purposes. The State pattern covers how processing is completed. The Singleton pattern covers how object instances are managed. Many software designs involve numbers of overlapping and complementary patterns.