DEV Community

Discussion on: Do you need singletons in Python ?

 
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 ( .... 🦗.... )