0

I'm confused about this code I got from here:

import functools

def singleton(cls):
    """Make a class a Singleton class (only one instance)"""
    @functools.wraps(cls)
    def wrapper_singleton(*args, **kwargs):
        if not wrapper_singleton.instance:
            wrapper_singleton.instance = cls(*args, **kwargs)
        return wrapper_singleton.instance
    print('****')
    wrapper_singleton.instance = None
    return wrapper_singleton

@singleton
class TheOne:
    pass

Why doesn't wrapper_singleton.instance = None set the instance to none each time the class is instantiated? I put a print statement above this line and it only gets called once also. Thanks

>>> first_one = TheOne()
>>> another_one = TheOne()

>>> id(first_one)
140094218762280

>>> id(another_one)
140094218762280

>>> first_one is another_one
True
Rik
  • 1,870
  • 3
  • 22
  • 35
  • 2
    Because `singleton` only gets called once, right here: `@singleton`. It's never called anywhere else. Note, there is a bug here, if the class implements `__bool__` to return false, it will generate a new instance. They probably should have said `if wrapper_singleton.instance is not None:`. – juanpa.arrivillaga Mar 14 '19 at 06:38

1 Answers1

1

Why doesn't wrapper_singleton.instance = None set the instance to none each time the class is instantiated?

Because that part of the code is only executed the when class is decorated.
This:

@singleton
class TheOne:
    pass

is functionally equivalent to

class TheOne:
    pass

TheOne = singleton(TheOne)

Both versions of the code actually return a function through the magic of functools.wraps, that acts as if it was the wrapped callable, as @smarie excellently explains here.

TheOne = singleton(TheOne)
print(TheOne)
# <function TheOne at 0x00000000029C4400>

If you remove the @functools.wraps decoration, you have a superficial look behind the scenes:

def singleton(cls)
    #@functools.wraps(cls)
    def wrapper_singleton(*args, **kwargs): ...

TheOne = singleton(TheOne)
print(TheOne)
# <function singleton.<locals>.wrapper_singleton at 0x00000000029F4400>

So the name TheOne is actually assigned to the inner function wrapper_singleton of your singleton function.
Hence when you do TheOne(), you don't instantiate the class directly, you call wrapper_singleton which does that for you.
This means, that the function singleton is only called when you decorate the class or do that manually via TheOne = singleton(TheOne). It defines wrapper_singleton, creates an additional attribute instance on it (so that if not wrapper_singleton.instance doesn't raise an AttributeError) and then returns it under the name TheOne.

You can break the singleton by decorating the class again.

class TheOne:
    def __init__(self, arg):
        self.arg = arg

TheOne = singleton(TheOne)
t1 = TheOne(42)
print(t1.arg, id(t1))
# 42 43808640

# Since this time around TheOne already is wrapper_singleton, wrapped by functools.wraps,
# You have to access your class object through the __wrapped__ attribute
TheOne = singleton(TheOne.__wrapped__)
t2 = TheOne(21)
print(t2.arg, id(t2))
# 21 43808920
shmee
  • 4,721
  • 2
  • 18
  • 27