HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Singleton base class

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
singletonclassbase

Problem

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 me

Solution

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.

Code Snippets

# -*- coding: utf-8 -*-
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__
class Logger:
    @singleton_new
    def __new__(cls):
        return super().__new__(cls)

    def log(self, text):
        print(text)
@classmethod
@singleton_new
def get_instance(cls):
    return super().__new__(cls)

Context

StackExchange Code Review Q#93539, answer score: 5

Revisions (0)

No revisions yet.