-
-
Save werediver/4396488 to your computer and use it in GitHub Desktop.
import threading | |
# Based on tornado.ioloop.IOLoop.instance() approach. | |
# See https://github.com/facebook/tornado | |
class SingletonMixin(object): | |
__singleton_lock = threading.Lock() | |
__singleton_instance = None | |
@classmethod | |
def instance(cls): | |
if not cls.__singleton_instance: | |
with cls.__singleton_lock: | |
if not cls.__singleton_instance: | |
cls.__singleton_instance = cls() | |
return cls.__singleton_instance | |
if __name__ == '__main__': | |
class A(SingletonMixin): | |
pass | |
class B(SingletonMixin): | |
pass | |
a, a2 = A.instance(), A.instance() | |
b, b2 = B.instance(), B.instance() | |
assert a is a2 | |
assert b is b2 | |
assert a is not b | |
print('a: %s\na2: %s' % (a, a2)) | |
print('b: %s\nb2: %s' % (b, b2)) |
What happens if somebody just calls another_a = A()
? Then you have a second instance...
This could be prevented by modifying the instance()
setting a temporary magic class attribute named _inside_instance
to True
(initially False
) c/w adding a __new__()
method, using the same lock (but should become Rlock
(re-entrant) instead of Lock
) checking whether _within_instance
was set (i.e. existed). If not, raise an Exception (e.g. RuntimeError
).
This all resulted in the following implementation (which I created before I spotted this one):
class SingletonMixin:
"""Mixin class to make your class a Singleton class."""
_instance = None
_rlock = RLock()
_inside_instance = False
@classmethod
def instance(cls: Type[T], *args: Any, **kwargs: Any) -> T:
"""Get *the* instance of the class, constructed when needed using (kw)args.
Return the instance of the class. If it did not yet exist, create it
by calling the "constructor" with whatever arguments and keyword arguments
provided.
This routine is thread-safe. It uses the *double-checked locking* design
pattern ``https://en.wikipedia.org/wiki/Double-checked_locking``_ for this.
:param args: Used for constructing the instance, when not performed yet.
:param kwargs: Used for constructing the instance, when not perfored yet.
:return: An instance of the class
"""
if cls._instance is not None:
return cls._instance
with cls._rlock:
# re-check, perhaps it was created in the mean time...
if cls._instance is None:
cls._inside_instance = True
try:
cls._instance = cls(*args, **kwargs)
finally:
cls._inside_instance = False
return cls._instance
def __new__(cls, *args, **kwargs):
"""Raise Exception when not called from the :func:``instance``_ class method.
This method raises RuntimeError when not called from the instance class method.
:param args: Arguments eventually passed to :func:``__init__``_.
:param kwargs: Keyword arguments eventually passed to :func:``__init__``_
:return: the created instance.
"""
if cls is SingletonMixin:
raise TypeError(f"Attempt to instantiate mixin class {cls.__qualname__}")
if cls._instance is None:
with cls._rlock:
if cls._instance is None and cls._inside_instance:
return super().__new__(cls, *args, **kwargs)
raise RuntimeError(
f"Attempt to create a {cls.__qualname__} instance outside of instance()"
)
@jhcloos seems cool!
@jhcloos seems cool!
But it would be like this:
if __name__ == '__main__':
class A(SingletonMixin):
pass
class B(SingletonMixin):
pass
a, a2 = A.instance(), A.instance()
b, b2 = B.instance(), B.instance()
assert a is a2
assert b is b2
assert a is not b # <== as a result, a is b in this case
print('a: %s\na2: %s' % (a, a2))
print('b: %s\nb2: %s' % (b, b2))
@sheldonldev - I don't understand what you mean. If I use my SingeletonMixin
just like you do in your example above, and I enter ...
>>> a = A.instance()
>>> a2 = A.instance()
>>> a is a2
True
>>> b = B.instance()
>>> a is b
False
... that's exactly the behaviour that we would like to see, right?
@sheldonldev - I don't understand what you mean. If I use my
SingeletonMixin
just like you do in your example above, and I enter ...>>> a = A.instance() >>> a2 = A.instance() >>> a is a2 True >>> b = B.instance() >>> a is b False
... that's exactly the behaviour that we would like to see, right?
@jhbuhrman Oh~~ I see. I use 'class' instead of 'cls', so I got all the same singleton! You are right. That's my fault, hh~
The
if not cls.__singleton_instance
is not duplicated, it is the double-checked locking pattern.