paopao00 2019-09-19
单例模式:一个来自“四人帮”著作中的“创造模式”
Gang of Four (GOF):设计模式四人组(“四人帮”,又称Gang of Four,即Erich Gamma, Richard Helm, Ralph Johnson & John Vlissides四人)的《设计模式》,原名《Design Patterns: Elements of Reusable Object-Oriented Software》(1995年出版,出版社:Addison Wesly Longman.Inc),第一次将设计模式提升到理论高度,并将之规范化。
Python程序员几乎从不会像“四人帮”著作中所描述的那样来实现单例模式,其单例类禁止正常的实例化,而是提供一个返回单例实例的类方法。Python更加优雅,它允许类继续支持用于实例化的常规语法,同时定义一个返回单例实例的自定义的__new__()方法。但是,如果你的设计强制要求你提供对单例对象的全局访问,那么一个更python化的方法是使用全局对象模式代替。
在面向对象设计模式社区定义“单例模式”之前,Python就已经在使用术语singleton了。因此,我们应该首先从区分Python中“singleton”的几个含义开始。
Python 2标准库没有包含单例模式的例子。但是它确实支持像None和Ellipsis这样的单例对象,该语言通过在__builtin__模块中为它们进行命名来以python化的全局对象模式提供了对它们的访问支持。但是它们的类是不可调用的:
然而,在Python 3中, 这些类被升级到使用单例模式:
对于那些需要一个总是返回None的快速调用的程序员来说,这使得他们的工作变得更容易,尽管这种情况很少见。在大多数Python项目中,这些类从来没有被调用过,其好处纯粹是理论上的。当Python程序员需要None对象时,他们就使用全局对象模式并简单地输入其名称。
“四人帮”所瞄准的C++语言在对象创建上采用了一种独特的语法,看起来像这样:
这一行C++代码表明new总是创建一个新的类实例——它从不返回一个单例。有了这种特殊的语法,它们提供单例对象的选项是什么呢?
Python代码是如何说明它们的方法的呢?Python缺乏new、protected和private的复杂性。另一种方法是在__init__()中引发一个异常,使正常的对象实例化成为不可能。然后,类方法可以使用一个dunder方法来创建对象,而不会触发异常:
这就成功地阻止了客户端通过调用类来创建新的实例:
相反,调用者会被指引去使用instance()类方法,它会创建并返回一个对象:
后续对instance()的调用会返回单例,而不重复初始化步骤(我们可以从“创建新实例”没有再次打印出来的事实中看到),这正是“四人帮”的意图:
对于在Python中实现最初的"四人帮"类方法,我可以想象有更复杂的方案,但是我认为上面的例子用最少的魔力最好地说明了最初的方案。由于"四人帮"的模式并不适合Python,所以我将抵制对其进行进一步迭代的诱惑,转而尝试如何在Python中最好地支持该模式。
从某种意义上说,Python一开始就比C++对单例模式有更好的准备,因为Python缺少一个强制创建新对象的new关键字。相反,对象是通过调用一个callable(可调用对象)来创建的,这对该callable实际执行的操作没有语法限制:
为了让开发者能够控制对类的调用,Python 2.4添加了__new__() 双下方法来支持相关的创建模式,比如单例模式和Flyweight模式。
Web上到处都是以__new__()为特色的单例模式方案,每个方案都提出了一种或多或少复杂的机制来处理该方法的最大怪癖: 不管返回的对象是否是新的,返回值总是会调用__init__()。为了让我自己的例子更简单,我将不定义一个__init__()方法,从而避免必须去处理它
对象会在首次调用类时被创建:
但是第二次调用会返回相同的实例。“正在创建对象”消息不会打印,也不会返回一个不同的对象:
上面的示例选择了简单性,但以在通常情况下执行两次cls._instance属性查询为代价。对于对这种浪费感到尴尬的程序员来说,他们当然可以为结果分配一个名称并在return语句中进行重用。可以想象,其它各种改进将会导致更快的字节码。但是,无论如何精心调整,上面的模式都是每个Python类的基础,每个Python类都在读法类似于普通的类实例化的东西后面隐藏了一个单例对象。
虽然“四人组”的原始单例模式与像Python这样一种缺乏 new, private, 和 protected概念的语言的适配性很差,但在__new__()顶上创建它时忽略该模式并非易事——毕竟,单例是引入__new__() 双下划线方法的部分原因!
但是Python中的单例模式确实存在一些缺点。
第一个反对意见是,对于许多Python程序员来说,单例模式的实现很难阅读。另一种替代的全局对象模式很容易阅读的:它只是熟悉的赋值语句,被放置在模块的顶部。但是,当一个Python程序员第一次阅读到一个__new__()方法时,他可能不得不停下来并查看文档来理解这是怎么一回事。
第二个反对意见是,单例模式会对类进行调用,比如Logger(),会误导读者。除非设计者已经在类名中添加了“单例”或其他一些提示,并且读者非常了解设计模式,能够理解这些提示,否则代码读起来就像在创建和返回一个新实例一样。
第三个反对意见是,单例模式会强制执行一个全局对象模式不会执行的设计承诺。提供一个全局对象仍然可以让程序员自由地创建该类的其他实例——这对测试来说是特别有用的,让它们每个都测试一个完全独立的对象,而不需要将共享对象重置回已知的良好状态。但是单例模式使附加实例变得不可能。(除非调用者愿意屈尊去使用猴子补丁修复; 或临时修改_instance,以颠覆__new__()中的逻辑; 或者创建一个替换该方法的子类。但你必须绕开的模式通常是你应该避免的模式。)
那么,你为什么要在Python中使用单例模式呢?
真正需要该模式的一种情况是已经存在一个类,由于有了新的需求,该类现在作为一个单个实例运行是最好的。如果不可能迁移所有客户机代码来停止直接调用该类并开始使用一个全局对象,那么单例模式将是一种自然而然的方法,它可以在保留旧语法的同时转向一个单例。
但是,最好是避免使用这种模式,最好遵循Python官方FAQ的建议并使用全局对象模式。
英文原文:https://python-patterns.guide/gang-of-four/singleton/
译者:好酒不上头