preface

Today’s group discussion covered the singleton pattern, which is probably the most familiar design pattern.

In simple terms, the singleton pattern ensures that an instance exists only once throughout the life cycle of a project, and that it is the same instance whenever it is used anywhere in the project.

The singleton pattern is simple, but it has some tricks that few people know about.

Edge cases

There are many ways to implement the singleton pattern in Python, and the one I used most often was the following.

class Singleton(object):
    
    _instance = None
    def __new__(cls, *args, **kw):
        if cls._instance is None:
            cls._instance = object.__new__(cls, *args, **kw)
        return cls._instance
Copy the code

There are two problems with this notation.

1. The singleton pattern cannot be instantiated with a parameter. Extend the above code to the following form.

class Singleton(object):
    
    _instance = None
    def __new__(cls, *args, **kw):
        if cls._instance is None:
            cls._instance = object.__new__(cls, *args, **kw)
        return cls._instance

    def __init(self, x, y):
        self.x = x
        self.y = y

s = Singleton(1.2)
Copy the code

TypeError: Object.__new__ () takes exactly one argument (the type to instantiate) error is raised

2. When multiple threads instantiate the Singleton class, it is possible to create multiple instances because it is likely that multiple threads will simultaneously determine that CLs._instance is None and enter the code that initializes the instance.

Implement singleton based on synchronous lock

Consider the second problem with the above implementation first.

Since multithreaded cases have boundary cases that parameter multiple instances, using synchronous locks to resolve multithreaded conflicts is possible.

import threading

# synchronization lock
def synchronous_lock(func):
    def wrapper(*args, **kwargs):
        with threading.Lock():
            return func(*args, **kwargs)
    return wrapper

class Singleton(object):
    instance = None

    @synchronous_lock
    def __new__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = object.__new__(cls, *args, **kwargs)
        return cls.instance
Copy the code

Synchronization of the singleton methods through threading.lock () in the code above will prevent the creation of multiple instances when dealing with multiple threads, so you can simply experiment.

def worker(a):
    s = Singleton()
    print(id(s))

def test(a):
    task = []
    for i in range(10):
        t = threading.Thread(target=worker)
        task.append(t)
    for i in task:
        i.start()
    for i in task:
        i.join()

test()
Copy the code

After running, the id of the printed singleton is the same.

A better way

With synchronization locks, there is no major problem other than the inability to pass in parameters, but is there a better solution? Does the singleton pattern have an implementation that accepts parameters?

Read the official Python wiki (wiki.python.org/moin/Python…

def singleton(cls):
    cls.__new_original__ = cls.__new__

    @functools.wraps(cls.__new__)
    def singleton_new(cls, *args, **kwargs):
        it = cls.__dict__.get('__it__')
        if it is not None:
            return it
        
        cls.__it__ = it = cls.__new_original__(cls, *args, **kwargs)
        it.__init_original__(*args, **kwargs)
        return it

    cls.__new__ = singleton_new
    cls.__init_original__ = cls.__init__
    cls.__init__ = object.__init__
    return cls
    
@singleton
class Foo(object):
    def __new__(cls, *args, **kwargs):
        cls.x = 10
        return object.__new__(cls)

    def __init__(self, x, y):
        assert self.x == 10
        self.x = x
        self.y = y

Copy the code

The singleton class decorator is defined in the above code, which is executed at precompilation time. Using this feature, the Singleton class decorator replaces the class’s __new__ and __init__ methods and instantiates the class using singleton_new methods. In the singleton_new method, the existence of the __it__ attribute in the attributes of the class is determined to determine whether to create a new instance. If so, the original __new__ method of the class is called to complete the instantiation and the original __init__ method is called to pass the parameters to the current class, thus completing the purpose of the singleton pattern.

This method allows the singleton class to accept the corresponding arguments, but in the face of multi-threaded simultaneous instantiation, multiple instances may occur, in which case a thread synchronization lock can be added.

def singleton(cls):
    cls.__new_original__ = cls.__new__
    @functools.wraps(cls.__new__)
    def singleton_new(cls, *args, **kwargs):
        # synchronization lock
        with threading.Lock():
            it = cls.__dict__.get('__it__')
            if it is not None:
                return it
            
            cls.__it__ = it = cls.__new_original__(cls, *args, **kwargs)
            it.__init_original__(*args, **kwargs)
            return it

    cls.__new__ = singleton_new
    cls.__init_original__ = cls.__init__
    cls.__init__ = object.__init__
    return cls
Copy the code

Additional consideration of whether to add synchronization locks

If a project does not need to use thread-dependent mechanisms, but uses a thread lock in a singleton, this is not necessary and will slow the project down.

Read the CPython thread module source code and you’ll see that Python does not initially initialize the thread-related environment. Instead, PyEval_InitThreads is used to initialize the thread-related environment when using the Theading library. The code snippet is shown below (I omitted a lot of irrelevant code).

static PyObject *
thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
{
    PyObject *func, *args, *keyw = NULL;
    struct bootstate *boot;
    unsigned long ident;
    
    // The interpreter is not initialized by default. It is initialized only when the user is using it.
    PyEval_InitThreads(); /* Start the interpreter's thread-awareness */
    // Create a thread
    ident = PyThread_start_new_thread(t_bootstrap, (void*) boot);
    
    // Returns the thread ID
    return PyLong_FromUnsignedLong(ident);
}
Copy the code

Why is that?

Because multithreaded environments can start GIL lock-related logic, this can slow down Python programs. Many simple Python programs do not need to use multithreading, do not need to initialize the thread-specific environment, and Python programs run faster without GIL locks.

If multithreading is not involved in your project, then there is no synchronization lock to implement the singleton pattern.

At the end

There are a lot of articles on the Internet about how Python implements singletons, and you only need to determine whether singletons can be guaranteed in multiple threads and whether initial arguments can be passed to singletons.

Welcome to lazy programming and explore the nature of technology.