前一阵子翻框架的时候看见了一个适用于 .NET Core 的 Telegram 机器人框架,感觉还不错,于是就上手试用了一下。在开发过程中涉及到一块保存管理员信息的需求,但是我本地有没有装数据库,也懒的研究怎么在 .NET 中使用 SQLite,更不想为了这点破事专门研究一下 MSSQL,于是,应该怎么实现呢?

我把 Root Admin 的信息写在了配置文件中,使用一个对象保存临时「提拔」的管理员。很明显,我不应该在运行时修改静态的配置文件(谁知道会搞出什么幺蛾子),但我还需要在不同的线程之间共享这个对象,于是乎,我对这个对象使用了单例模式(Singleton)

What's Singleton (Eh... Something weights a Single Ton?)

很明显不是指某个一吨 (single ton) 重的东西。

单例模式 (Singleton) 一般是指保证一个类只有一个实例,并给这个实例提供一个全局的访问点。

Something about static variables

在单例模式中,最重要的一环就是静态变量(static variable),构造完成的实例保存在一个静态变量中,这样,这个 属性(property) 便成了这个类 (class) 的一部分,即便是不进行实例化或再次进行实例化,只要不直接修改这个属性的值,它就可以一直保持当前状态。

单例模式利用了静态变量的这个特点来保存构造完成的实例,同时将构造函数 (Constructor) 私有化,使之不可以从外部调用,从而避免了重复构造该对象,以此保证实例的唯一性。


Singleton in C#

class SingletonClass
{
    private static SingletonClass _instance = null;
    private static readonly object Padlock = new object();
    
    private SingletonClass() 
    {
        // 私有构造函数
    }
    
    public static SingletonClass Instance {
        get {
            lock (Padlock)
            {
                // 防止多个线程同时调用构造函数
                return _instance ?? (_instance = new SingletonClass());
            }
        }
    }
}

In Python ?

Python's __new__()

众所周知,Python 的类没有严格意义上的私有成员,也不可以将像某些语言那样直接声明构造函数是私有的,所以为了避免构造函数被重复调用,需要另辟蹊径。

Python 在构造一个实例时,首先调用 __new__(cls, *args, **kwargs),这个静态方法调用这个类(__new__() 的第一个参数 cls)的__init__(*args, **kwargs) ,构造出这个类的一个实例,之后将之以返回值的形式抛给外层,完成整个构造过程。

所以,我们可以通过修改 __new__() 的行为来防止这个类被重复构造。在 __new__() 的时候首先检查有没有已经构造完成的实例,如果有的话,直接返回该实例,而不是继续执行新的构造过程,这样就可以防止同一个类被重复构造。

Mutable vs Immutable objects

在上一小节中,我们可以通过干预 __new__(cls [, ...]) 来干预类的构造过程,我们还需要想办法让这个初始化后的对象可以持久化在这个类中。

与上文 C# 代码相同的原理,在类中保存一个静态的属性(property),很多文章中都使用一个字典来保存这个值。很明显,我直接在类中通过一个变量保存这个构造好的对象也是可以实现的,我猜想这可能是因为修改字典内的数据不会影响到该字典本身的值,这样做会更安全。

Singleton in Python (Example code)

class Singleton(object):
    __instance = {}
    __key = 'ins'
    
    def __new__(cls, *args, **kwargs):
        # 个人认为此处应加锁
        if cls.__key in cls.__instance.keys():
            return cls.__instance[cls.__key]
        
        ins = super().__new__(cls, *args, **kwargs)
        cls.__instance[cls.__key] = ins
        return ins

When to use? Some thoughts.

Why Singleton seems not usually used in Web Development

And...other coroutine based frameworks?


Thread Safety

如果你是在使用 CPython,那你基本不用担心 Singleton 的线程安全问题。

通常来说,CPython 中任何基于 PyObject 的对象都会受到 GIL 的限制,无法同时被多个线程访问。当然,这并不是绝对,但根据查找资料的结果来看,向变量赋值以及直接向字典中的具体元素赋值这个操作是线程安全的,不必额外进行处理。

个人觉得,如果对这件事不放心,可以对 __new__ 加锁,就像上面 C# 代码中那样。


References

  1. object.__new__() in Python 2.7.15 Document

  2. StackOverflow: Creating a singleton in Python

  3. Python Design Patterns - Singleton

  4. What kinds of global value mutation are thread-safe?