V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
mimzy
V2EX  ›  Python

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

  •  
  •   mimzy ·
    mookrs · 2018-08-17 08:54:43 +08:00 · 2338 次点击
    这是一个创建于 2077 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在读《编写高质量代码:改善 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 中子类和父类中方法加载的顺序,因此不太明白作者说的这两个问题是什么意思?是否有可能举出例子呢?谢谢~

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

    ```
    class Derive(Singleton):

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

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

    ```

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

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

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

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

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

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

    @HelloAmadeus #10 收到,明白~
    mimzy
        12
    mimzy  
    OP
       2018-08-17 11:17:56 +08:00
    第 2 条也理解了,# 7 的解释很详细,书中的原文这样表达可能比较好:如果子类有 __init__() 方法,那么每次实例化该**子类**的时候,__init__() 都会被调用到(按道理应该只被调用一次)。
    Hk4Fun
        13
    Hk4Fun  
       2018-08-17 11:43:15 +08:00
    用装饰器应该可以保证__init__()只被调用一次
    mimzy
        14
    mimzy  
    OP
       2018-08-17 12:09:26 +08:00 via Android
    @Hk4Fun 装饰器可能存在一个问题,用装饰器修饰的单例类不能再有子类,否则使用子类时会出错。模块导入应该是最完美的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5349 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 07:31 · PVG 15:31 · LAX 00:31 · JFK 03:31
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.