DEV Community

Cover image for Do you need singletons in Python ?
Tai Kedzierski
Tai Kedzierski

Posted on

Do you need singletons in Python ?

(Image (C) Tai Kedzierski)

Object orientation

The typical example of object orientation goes like this:

  • You have a concept of a thing, which you define in your class, like, the idea of "a car" (class Car)
  • You have an object instance of a class, representing a tangible item relating to that concept, like a Renault Laguna (new Car("Renault", "Laguna")

This makes sense - we have a concept of a car being drivable, ridable, etc which applies to all actual cars.

In the language of object orientation, it began to be stated that everything is an object (and by extension, everything has a class).

This falls down in some scenarios in the real world:

Let's say we're working on a project on a Raspberry Pi. And that Pi has access to the GPIO board (for simplicity, there is a board, it has electric circuits that can be closed or opened). And let's, arbitrarily but not unreasonably, state that the Pi is only ever has one set of GPIO pins on it at a time.

Singleton

If we were doing this in Java, we'd probably model class GpioBoard extends Singleton (or however we do that in Java in reality - treat as pseudo-code) because we don't want several instances of GpioBoard being retrieved and configured differnetly in different parts of the program. The effect of extending Singleton means that when the class tries to instantiate, the environment checks for an existing instance to return, and only creates a new one if none such already exists.

With such an approach in Python, we end up with a class in which we need to implement the singleton pattern explicitly, an instantiation, and a call

# FILE: gpio.py

class GpioBoard(Singleton):
  _instance = None

  def __new__(cls):
    # From https://python-patterns.guide/gang-of-four/singleton/
    if cls._instance is None:
      print('Creating the object')
      cls._instance = super(Logger, cls).__new__(cls)
      # .... initialization code ...
    return cls._instance

  def set_pin(self, value):
    ... # implementation
Enter fullscreen mode Exit fullscreen mode
# FILE: main.py

from gpio import GpioBoard

board = GpioBoard()
board.set_pin(1)
Enter fullscreen mode Exit fullscreen mode

A module is a singleton

Here's the thing though - in Python a module itself is a singleton. The following code does exactly the same, without the additional code:

# FILE: gpio.py

def __setup():
    # .... initialization code ...

def set_pin(value):
  ... # implementation

# Single runtime entrypoint only gets executed once
#  even from multiple imports of the same module
__setup()
Enter fullscreen mode Exit fullscreen mode
# FILE: main.py

import gpio

# Immediately ready to use, would you look at that ...
gpio.set_pin(1)
Enter fullscreen mode Exit fullscreen mode

So much less code. No singleton management. Module is an inherent singleton.

What if there are multiple GPIO extension boards?

Or more generally, what if the item that was a singleton in the original understanding of the problem/design of the architecture becomes no longer a singleton ?

In this case, there may yet be re-work to be done. An architectural change can cause base assumptions to become false in later iterations of a design, and re-working what were once module-level variables and methods into a regular object-oriented design becomes a little (or huge) juggling task ; and anything that once simply imported our library now would need to be updated to get a class and instantiate an object explicitly.

Do we need singletons in Python?

I actually set out to deliver an emphatic "no" to the question. From the point of view of "are singletons the answer to enforcing single-instance (in Python)?": no, using a module and module-level methods without a class definition is perfectly satisfactory.

But I don't really have a firm yes/no answer at this point. Architectures being fluid, it may yet make sense to implement everything as an object from the get-go , and be able to de/singleton-ise back and forth as needed. Is explictly implementing singleton patterns a case of YAGNI? Who knows.

Top comments (7)

Collapse
 
xtofl profile image
xtofl

Accidentally bumped onto this post, that goes full-out summing up arguments against: dev.to/mcsee/singleton-the-root-of....

Collapse
 
xtofl profile image
xtofl

I think: NO. I haven't seen a situation where the Singleton pattern could not be replaced by proper design (separating construction from use, in other words, Dependency Injection). I have seen no Singleton that was properly tested.

Using the module as a means to enforce single-instance removes the enforcing from the programmer, which is a nice thing. It does, however, make it impossible to test the code in isolation because of the side-effects.

Collapse
 
taikedz profile image
Tai Kedzierski

Could you elaborate a bit please? "separating construction from use" in the context of needing to ensure and enforce a unique point of access to a resource does sound like a Singleton pattern to me... at least, the way I've interpreted Singleton.

I do see the issue with using the module as encofrcement creating a non-injectable situation though, from a testing standpoint.

Collapse
 
xtofl profile image
xtofl

You're right: dependency injection (DI) leaves the enforcing to the designer, while Singleton would be guarded by the language, which is more limiting w.r.t. allocation. It leads to too many problems for me to trust, though.

DI is also a technique to indicate that locating the point of access is not the concern of the consumer. It removes the consumer's coupling to the class that represents the resource under control. By separating the provide/consume concerns, it becomes clear by the design that the resource is not allocated by the consumer, but by the provider.

The uniqueness follows from the fact that the resource is allocated 'upstream'. There is only one entry point in the program; that main method is responsible for allocation - directly or indirectly - and forwarding the resources to the consumer part.

I have applied this design for the control software of a sorting machine - resisting a classic candidate for sneaking in a Singleton: there's only one controller board, after all, and all of the components need access to it.

When it was decided to increase the sorting capacity by doubling the hardware, none of the consuming software had to be adapted.

For me, eating that pudding that was proof enough that dependency injection leads to manageable (and scalable!) software. And that we can live happily without the superficial 'comfort' of a global-access instance.

Maybe this application of DI needs a catchy pattern name, too :).

Thread Thread
 
taikedz profile image
Tai Kedzierski

OK - so the way I am understanding it is something like as follows...

_INSTANCE = None

# Private instantiable object
#  dundered to "force" consumer to use `get_controller_board()`
class __ControllerBoard(__InternalClass):
    ...
    # all the logic for actual controller board interaction

# Controller board access manager
# consumers of this controller board module start here
def get_controller_board():
    global _INSTANCE
    if _INSTANCE is None:
        _INSTANCE = __ControllerBoard()
    return _INSTANCE
Enter fullscreen mode Exit fullscreen mode

I might have written this clunkily, but does that illustrate the principle?

I don't actually implement a "Singleton" approach specifically on the controller board class, leaving extension possible, but at the same time I do enforce a single means to access a controller board.

The module._INSTANCE property can be accessed externally, but can only be initialised by get_controller_board()

The module._INSTANCE property also is injectable at this point, causing get_controller_board() to return our injected instance as needed in test?

Thread Thread
 
xtofl profile image
xtofl

...
Not exactly. This moves the singleton implementation logic to the module, but doesn't change its general principle. Better than the class, but
not flexible, and hardly testable (think test case interdependencies). It does prove that no Singletons are needed in Python, even if you want to reify the pattern.

What I meant was inverting control: the consumer does not request the resource at run time, but is injected with the resource by its controlling module at construction time. The life of the resource is controlled by the controlling module:

# ui.py - consuming module

class Led:
  def __init__(self, gpio, pin):  # resource is injected!
    self.gpio = gpio
    self.pin = pin

  def blink(self, times: int):
    for _ in range(times):
      self.gpio.pin(self.pin, 1)
      time.sleep(.5)
      self.gpio.pin(self.pin, 0)
      time.sleep(.5)
...
Enter fullscreen mode Exit fullscreen mode
# test_ui.py
@pytest.mark.parametrize("ledpin", range(120))
@pytest.parallelize_and_random_order
def test_led_blinking_forwards_to_gpio(ledpin):
  gpio = MagicMock()  # resource is mocked
  led = Led(gpio, pin=ledpin)
  led.blink(times=5)
  assert gpio.pin(ledpin, 0).was_called(5)
  assert gpio.pin(ledpin, 1).was_called(5)
Enter fullscreen mode Exit fullscreen mode
# blinker.py: the Controlling Module

board = PI()  # resource is controlled here
leds = [Led(gpio=board.gpio, pin=i) for i in range(10)]
buttons = [Button(gpio=board.gpio, pin=i) for i in range(10, 20)]

while not buttons[5].down():
  leds[2].blink()
Enter fullscreen mode Exit fullscreen mode

This design allows scaling, testing, mutating, whatnot without touching the consumer. It stays clear from global limitation of the number of resources, but still results in no accidental overallocation. That is achieved by separating the allocation from the retrieval.

# distributed_blinker.py
# there are many boards, controlled here
warning_board = RemotePI("192.168.1.95")
display_board = RemotePI("192.168.1.211")
warning_led = Led(warning_board.gpio, 1)
good_led = Led(display_board.gpio, 1)
stop_check = Button(display_board.gpio, 2)
while stop_check.down():
  good_led.blink()
warning_led.blink()
Enter fullscreen mode Exit fullscreen mode

Dang, whenever I discuss something with you, I end up writing an article :).

Thread Thread
 
taikedz profile image
Tai Kedzierski

Ah... an item that needs a shared resource receives the reference to said resource on instantiation , and does not tryo to go looking for it... I think I get it....

We have a situation where due to our testing framework itself (and an old inherited testing fixture), our instance does not have control of the instantiation of the TestCase (nor do we specify the main() function).... and that's why I was not thinking in that direction.

But this actually does make more sense to me... probably.

Dang, whenever I discuss something with you, I end up writing an article :).

Glad to be of service 🤣 And thank you, you have LED me to enlightenment ( .... 🦗.... )