3
\$\begingroup\$

I just wrote a singleton base class for a hobby project I'm currently working on. Python is not my first language, which is why I am not sure if everything is done the pythonic way.

I also used multiple resources/tutorials/articles for Python2 and 3. My singleton is supposed to be Python3 compatible. The implementation should be thread safe, which is why I enter a lock while creating the instance.

To allow custom initializations I decided to let the user provide a create_class function where the object could be created and initialized. The base class is more like a "singleton-factory" than a "classical" singleton where the object itself provides both (logic and singleton).

Please tell me what can I do better, more efficient, and more pythonic.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
  this module implements provides a baseclass for
  a singleton.

  it allows you to simply create singleton objects
  by overriding the PyRlSingleton class.

  it gives you access to the singleton instance in
  a explicit way via a "get_instance()" function.

  if you mistakingly try to create your singleton
  class it raises an exception telling you that this
  is not allowed.

  you have to provide a "create_instance()" function
  which creates your singleton implementation; this
  is automatically called when the singleton is used
  the first time.

  the creating process is thread safe, so you can use
  it in a multithreading environment.

  see the "main()" function for a simple example.

  have fun!
"""

import _thread # allocate_lock()

class PyRlSingletonMeta(type):
  def __call__(cls, *args, **kwargs):
    """
      this method disallows creation of a
      singleton via a ClassName() call
    """
    raise Exception("not allowed, use get_instance()")

class PyRlSingleton(object, metaclass = PyRlSingletonMeta):
  __lock = _thread.allocate_lock()
  __inst = None

  @classmethod
  def create_instance(cls):
    """
      this is the default create_instance method
      you should provide your own via overriding.
    """
    raise Exception("create_instance() missing in {0}".format(cls.__name__))

  @classmethod
  def get_instance(cls):
    """
      this classmethod returns always the same instance
      of cls. it creates an object via calling the
      create_instance class method when it is first
      called.
    """
    if cls.__inst == None:
      with cls.__lock:
        if cls.__inst == None:
          cls.__inst = cls.create_instance()
    return cls.__inst

def main():
  """ test the singleton in a simple example, a logger """

  class LoggerImpl(object):
    """ my class supposed to be a singleton """
    def log(self, text):
      print(text)

  class Logger(PyRlSingleton):
    """
      the singleton itself. i have to override the
      create_instance() class method and let it return
      my LoggerImpl.
    """
    @classmethod
    def create_instance(cls):
      """ create and initialize your implementation here. """
      return LoggerImpl()

  Logger.get_instance().log("hello world")
  l1 = Logger.get_instance()
  l2 = Logger.get_instance()
  if l1 != l2:
    raise Exception("what?")
  if l1 != Logger.get_instance():
    raise Exception("ugh!?")

if __name__ == '__main__':
  main()
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

This line isn't needed in Python 3:

# -*- coding: utf-8 -*-

so you should probably just remove it. UTF-8 is assumed.

Your docstring is indented - why? It's not technically wrong, but it seem like a really arbitrary choice.

I'm not sure how to say this, but none of your sentences are capitalized. If this were a professional code-base, I would absolutely require good grammar and care of documentation. It is, after all, the primary text that represents the code.

I see _thread - you should be using the main threading interface. In particular, use threading.Lock instead of _thread.allocate_lock.

Since you're using Python 3, don't inherit from object - it's a no-op.

You have Logger.get_instance() return an instance of a different class. By this point, one should probably just make a get_logger function instead. Since that's not ideal, perhaps just overload __new__. You can do this with a metaclass, but a decorator is much simpler:

import threading
from functools import wraps

def singleton_new(make_instance):
    lock = threading.Lock()
    instance = None

    @wraps(make_instance)
    def __new__(cls):
        nonlocal instance

        if instance is None:
            with lock:
                if instance is None:
                    instance = make_instance(cls)

        return instance

    return __new__

This is as simple to use as

class Logger:
    @singleton_new
    def __new__(cls):
        return super().__new__(cls)

    def log(self, text):
        print(text)

and doesn't even affect the class' attributes, due to the magic of closures. One could use a class decorator instead, but I think this looks nicest. Note that this also allows usage on classmethods like

@classmethod
@singleton_new
def get_instance(cls):
    return super().__new__(cls)

Past that, I advise you to stick to PEP 8 where you can, which primarily means minor changes for your code.


On a design level, I strongly suggest rethinking using singletons at all. Globals are almost always better in such cases, and for the case of logging singletons aren't apropriate anyway (look instead at neat things like Logbook).

Even if you do conclude, for some reason, that you want singletons, at least consider the Borg pattern instead, which is mildly less aggrivating.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for your great answer, I will refactor my code and learn more about the Borg pattern this sounds very interesting and so are decorators. \$\endgroup\$
    – linluk
    Jun 14, 2015 at 11:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.