Skip to content

Instantly share code, notes, and snippets.

@werediver
Created December 28, 2012 09:51
Show Gist options
  • Save werediver/4396488 to your computer and use it in GitHub Desktop.
Save werediver/4396488 to your computer and use it in GitHub Desktop.
A thread safe implementation of singleton pattern in Python. Based on tornado.ioloop.IOLoop.instance() approach.
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))
@livioribeiro
Copy link

The if not cls.__singleton_instance is not duplicated, it is the double-checked locking pattern.

@jhbuhrman
Copy link

jhbuhrman commented Apr 16, 2020

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()"
        )

@sheldonldev
Copy link

@jhcloos seems cool!

@sheldonldev
Copy link

sheldonldev commented Mar 2, 2024

@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))

@jhbuhrman
Copy link

@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
Copy link

sheldonldev commented Mar 2, 2024

@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~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment