关于使用 __new__ 方法创建带锁的单例模式可能产生的问题

2018-08-17 08:54:43 +08:00
 mimzy

最近在读《编写高质量代码:改善 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]

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

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

2371 次点击
所在节点    Python
14 条回复
HelloAmadeus
2018-08-17 09:31:03 +08:00
init 和 new 的魔法方法和普通方法表现一样。子类 override 了父类的方法,要想有父类方法的行为,必须显式的调用父类方法。还有 Python 单例一般通过模块导入实现,模块导入是线程安全的。当然也可以通过原类的__call__方法来实现。学习设计模式是学习思想,具体实现要看语言特性,不要拘泥于一种实现方式。
yufpga
2018-08-17 09:34:12 +08:00
是这样的假如你有一个类继承了 Singleton, 并重载了__new__方法:

```
class Derive(Singleton):

```
yufpga
2018-08-17 09:43:47 +08:00
是这样的假如你有一个类继承了 Singleton, 并重载了__new__方法:

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

```

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

在 Python 中,由于 Python 的 import 机制和文件作用域,因此建议通过此来实现单例,这个和 c++等语言有些不同
mimzy
2018-08-17 09:51:06 +08:00
@HelloAmadeus #1 谢谢!模块导入的确是又方便又安全的一种方法

另外我感觉作者说的第 1 条其实是想表达 Override (重写)而不是 Overload (重载),这样就跟你的解释一致了…
mimzy
2018-08-17 09:53:53 +08:00
@yufpga #3 谢谢!第 1 条已经看这个代码明白了~

就是不知道第 2 条是否有更多的解释…
Mutoo
2018-08-17 09:59:51 +08:00
Singleton 可以被继承就不叫单例了。通过直接继承但不修改原有方法,就可以 fork 出另一个实例了,这已经违反了单例模式。对脚本语言来说,全局唯一实例根本不需要用面向对象的方法来保证。而 c++ 之类的静态语言可以用模版而不是继承的方式实现不同单例。
lxy42
2018-08-17 10:14:30 +08:00
关于类的创建、实例的创建和实例初始化,需要掌握一点元类的知识。

Singleton.__new__方法负责创建实例,然后 Python 内部尝试调用__init__方法初始化实例。因此,如果 Singleton 的子类定义了__init__方法,每次创建实例后 Python 都会调用__init__方法初始化实例,如果没有找到__init__方法,Python 就会一直往父类查找__init__,直至 object 为止。
lxy42
2018-08-17 10:34:55 +08:00
仅供参考: https://gist.github.com/ausaki/46ec0fec6a5d3684437380a9b21e5b13

在元类中实现单例,__init__方法只会调用一次。
josephshen
2018-08-17 10:39:27 +08:00
请务必立马扔掉这本书或者带上强烈批判的眼镜来看,这本书质量奇差,里面有大量的严重错误,简单概念复杂化,设计模式那部分明显是带着作者 Java 背景来写的,最恐怖的事情是国内圈子居然大部分都说好,我真替他们害臊
HelloAmadeus
2018-08-17 10:43:11 +08:00
@mimzy Python 是没有重载的
mimzy
2018-08-17 10:51:27 +08:00
@josephshen #9 哈哈哈我懂,主要过一遍看看有没有遗漏的小技巧,Python Cookbook 和 Fluent Python 才是真的好~

@HelloAmadeus #10 收到,明白~
mimzy
2018-08-17 11:17:56 +08:00
第 2 条也理解了,# 7 的解释很详细,书中的原文这样表达可能比较好:如果子类有 __init__() 方法,那么每次实例化该**子类**的时候,__init__() 都会被调用到(按道理应该只被调用一次)。
Hk4Fun
2018-08-17 11:43:15 +08:00
用装饰器应该可以保证__init__()只被调用一次
mimzy
2018-08-17 12:09:26 +08:00
@Hk4Fun 装饰器可能存在一个问题,用装饰器修饰的单例类不能再有子类,否则使用子类时会出错。模块导入应该是最完美的。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/480591

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX