Python中使用__new__方法创建带锁的单例模式可能产生哪些问题?

最近在读《编写高质量代码:改善 Python 程序的 91 个建议》这本书,我在作者给出的双检查锁单例模式基础上做了一点改写,精简了冗余的部分,如下:

import threading

class Singleton: _instances = {} _instance_lock = threading.Lock()

def __new__(cls, *args, **kwargs):
    if cls not in cls._instances:
        with cls._instance_lock:
            if cls not in cls._instances:
                cls._instances[cls] = super(Singleton, cls).__new__(cls, *args, **kwargs)
    return cls._instances[cls]

但是作者指出这个版本的单例有两个问题:

  • 如果 Singleton 的子类重载了 __new__() 方法,会覆盖或干扰 Singleton 类中 __new__() 的执行,虽然这种情况出现的概率极小,但不可忽视。
  • 如果子类有 __init__() 方法,那么每次实例化该 Singleton 的时候,__init__() 都会被调用到,这显然是不应该的,__init__() 只应该在创建实例的时候被调用一次。

我不太理解 Python 中子类和父类中方法加载的顺序,因此不太明白作者说的这两个问题是什么意思?是否有可能举出例子呢?谢谢~


Python中使用__new__方法创建带锁的单例模式可能产生哪些问题?

13 回复

init 和 new 的魔法方法和普通方法表现一样。子类 override 了父类的方法,要想有父类方法的行为,必须显式的调用父类方法。还有 Python 单例一般通过模块导入实现,模块导入是线程安全的。当然也可以通过原类的__call__方法来实现。学习设计模式是学习思想,具体实现要看语言特性,不要拘泥于一种实现方式。


在Python里用__new__加锁做线程安全的单例,一个经典写法是:

import threading

class Singleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

这个双重检查锁定(DCL)模式主要存在两个问题:

  1. 内存一致性/可见性问题:在CPython里,由于GIL的存在,这个特定代码通常能工作。但从语言规范和跨实现(如PyPy)的角度看,cls._instance = super().__new__(cls)这行赋值操作,其内存写入可能不会立即被其他CPU核心或线程看到。这意味着某个线程可能拿到了一个已分配但未完全初始化的对象实例。虽然Python的GIL在大多数情况下掩盖了这个问题,但它理论上存在,不符合严格的内存模型。

  2. 可读性与Pythonic问题:对于绝大多数场景,Python社区更推崇使用模块导入机制(模块在首次导入时天然是单例)或元类(__call__)来实现单例。用__new__加锁显得比较底层和冗长,容易引入不必要的复杂性。

总结:用模块或元类更简单可靠。

是这样的假如你有一个类继承了 Singleton, 并重载了__new__方法:

<br>class Derive(Singleton):<br> def __new__(cls):<br> # super().__new__() # 不小心忘记了<br> pass<br><br>

如果你在子类的__new__方法中忘记这是一个单例类, 你很可能会忘记显式的执行父类中的__new__方法,这时候父类中单例的那部分逻辑是不会执行的, 这时候 Derive 创建对象并不是单例的,这显然与你的预期是不符和的。

在 Python 中,由于 Python 的 import 机制和文件作用域,因此建议通过此来实现单例,这个和 c++等语言有些不同

#1 谢谢!模块导入的确是又方便又安全的一种方法

另外我感觉作者说的第 1 条其实是想表达 Override (重写)而不是 Overload (重载),这样就跟你的解释一致了…

#3 谢谢!第 1 条已经看这个代码明白了~

就是不知道第 2 条是否有更多的解释…

Singleton 可以被继承就不叫单例了。通过直接继承但不修改原有方法,就可以 fork 出另一个实例了,这已经违反了单例模式。对脚本语言来说,全局唯一实例根本不需要用面向对象的方法来保证。而 c++ 之类的静态语言可以用模版而不是继承的方式实现不同单例。

关于类的创建、实例的创建和实例初始化,需要掌握一点元类的知识。

Singleton.new__方法负责创建实例,然后 Python 内部尝试调用__init__方法初始化实例。因此,如果 Singleton 的子类定义了__init__方法,每次创建实例后 Python 都会调用__init__方法初始化实例,如果没有找到__init__方法,Python 就会一直往父类查找__init,直至 object 为止。

仅供参考: https://gist.github.com/ausaki/46ec0fec6a5d3684437380a9b21e5b13

在元类中实现单例,__init__方法只会调用一次。

请务必立马扔掉这本书或者带上强烈批判的眼镜来看,这本书质量奇差,里面有大量的严重错误,简单概念复杂化,设计模式那部分明显是带着作者 Java 背景来写的,最恐怖的事情是国内圈子居然大部分都说好,我真替他们害臊

#9 哈哈哈我懂,主要过一遍看看有没有遗漏的小技巧,Python Cookbook 和 Fluent Python 才是真的好~

#10 收到,明白~

第 2 条也理解了,# 7 的解释很详细,书中的原文这样表达可能比较好:如果子类有 init() 方法,那么每次实例化该子类的时候,init() 都会被调用到(按道理应该只被调用一次)。

用装饰器应该可以保证__init__()只被调用一次

装饰器可能存在一个问题,用装饰器修饰的单例类不能再有子类,否则使用子类时会出错。模块导入应该是最完美的。

回到顶部