singleton 是最简单的 design pattern:一个恰好只有一个 instance 的 class。 在使用经典实现之前,先问问 language 是否已经解决了这个问题; 这正是 When a Pattern Dissolves 对每一种 pattern 都提出的问题。 对 singleton 而言,Python 已经解决了。

Module 本身就是 Singleton

正如 06 · 模块与包 所展示的, Python 对每个 module 只 import 一次,并把它缓存到 sys.modules 中。 第一次之后的每次 import 都会得到同一个 module object。 module 就是 singleton,而 module level 定义的任何东西都会被所有人共享, 在整个 interpreter 中只有一份。 是一个 interpreter,不是一台 machine。 process pool 或 InterpreterPoolExecutor 会让每个 worker 拥有自己的 sys.modules, 因此每个 worker 都会构建自己的副本,而且一个 worker 中的写入对其余 worker 不可见。 singleton 只在持有它的 interpreter 内部是 single, 本章中的每种形式都是如此,而不只是 module。

把 state 放进一个 module:

# config.py
print("config body runs")
settings: dict[str, str] = {}
# module_singleton.py
import config
import config as again

print(config is again, config.settings is again.settings)
#: config body runs
#: True True

两个 import statement,只打印一行。 第一个会从头到尾执行 config.py,并以名称 config 为 key, 把得到的 module object 存进 sys.modules。 第二个会在那里找到它并跳过执行, 所以 body 永远不会执行两次,也只会构建一个 settings dict。 这就是 singleton:并不是 class 强制执行某条规则, 而是 import system 完成的一次 lookup。

从任何地方 import settings,拿到的都是同一个 dict。 通过某一次 import 对它进行 mutation,会对其他所有 import 可见:

# shared_config.py
from config import settings

settings["theme"] = "dark"
print(settings)
#: config body runs
#: {'theme': 'dark'}

没有 class,也没有额外仪式。 对大多数 singleton 需求,一个 module 就能解决问题。

共享之所以成立,是因为 mutation。 静悄悄终止这种共享的错误则是 rebinding。 from config import settings 会在你的 module 中创建自己的一个 name, 它指向 config.settings 所指向的同一个 dict object。 通过你的 name 进行 mutation,例如 settings["theme"] = "dark", 会修改那个共享 object,因此每个 module 都能看到变化。 但你在自己的 module 中执行 settings = {},只会 rebind 你这个 module 的 name, 两个 module 会悄无声息地分叉: config.settings 仍然保存旧 dict, 而你的代码现在指向的是一个私有的新 dict。 如果要替换整个 value,请通过 module 操作:先 import config, 再执行 config.settings = {...}。 通过任何 name 做 mutation 都可以。 rebinding 只通过 module 做。

共享还依赖于 name。 sys.modules 使用 module name 作为 key, 而你直接启动的文件以名称 __main__ 运行。 如果这个文件就是 config.py,稍后的 import config 找不到已缓存的 entry, 于是会再次执行 body,构建第二个 module object,以及它自己的一份 settings。 所以 singleton state 应该放在一个被 import 的 module 里, 而不是放在你直接运行的 script 里。

如果你想要 Class,就缓存 Instance

每一次 construction 都应该返回同一个 object。 最简单的办法,是把 construction 藏在一个 cached factory 后面。 这个 factory 把 functools.cache 应用到一个 constructor function 上; constructor function 就是一个普通 function,它构造并返回某个 class 的 instance。 它取代对 class constructor 的直接调用。

functools.cache(参见 Caching) 会对一个 function 做 memoize。 对于某一组 arguments,第一次 call 会运行 function 并存储 result。 之后用同一组 arguments 重复 call,都直接返回已存储的 result。 一个没有 arguments 的 constructor function 只有一种可能的 call, 所以对它做 cache 后,只会构造 instance 一次,此后永远返回同一个 object:

# singleton_cached_factory.py
from dataclasses import dataclass, field
from functools import cache

@dataclass
class Settings:
    data: dict[str, str] = field(default_factory=dict)

@cache
def settings() -> Settings:
    return Settings()

a = settings()
b = settings()
assert a is b
a.data["theme"] = "dark"
print(b)
#: Settings(data={'theme': 'dark'})

没有什么能让 Class 真正 Private

没有任何机制能阻止 caller 写下 Settings(),从而得到第二个 instance。 把 class 命名成 _Settings 会把它标记为 internal, 并让它不会出现在 from module import * 中; Python 对此也就做到这里。 双下划线并不是这种约定的更强版本。 在 module level,compiler 什么也不会 mangle, 所以第二个下划线什么也隐藏不了; 而在 class body 内部,compiler 会把 m.__Settings 重写为对 _TheClass__Settings 的 lookup, 反而会把这个 reference 搞坏。