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