单例设计模式

假设你在开发一个程序,它的所有配置项都保存在一个特定文件中。在项目启动时,程序需要从配置文件中读取所有配置项,然后将其加载进内存供其他模块使用。

由于程序执行时只需要一个全局的配置对象,因此你觉得这个场景非常适合使用经典设计模式:单例模式(singleton pattern)。

下面的代码就应用了单例模式的配置类 AppConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AppConfig:
"""程序配置类,使用单例模式"""

_instance = None

def __new__(cls):
if cls._instance is None:
inst = super().__new__(cls)
# 省略:从外部配置文件读取配置
...
cls._instance = inst
return cls._instance

def get_database(self):
"""读取数据库配置"""
...

def reload(self):
"""重新读取配置文件,刷新配置"""
...
```
在 Python 中,实现单例模式的方式有很多,而上面这种最为常见,它通过重写类的 __new__ 方法来接管实例创建行为。当 __new__ 方法被重写后,类的每次实例化返回的不再是新实例,而是同一个已经初始化的旧实例 cls._instance:

c1 = AppConfig()
c2 = AppConfig()
c1 is c2 ➊
True

1
2
3
❶ 测试单例模式,调用 AppConfig() 总是会产生同一个对象

基于上面的设计,如果其他人想读取数据库配置,代码需要这样写:

from project.config import AppConfig

db_conf = AppConfig().get_database()

重新加载配置

AppConfig().reload()

1
2
3
4
5
6

虽然在处理这种全局配置对象时,单例模式是一种行之有效的解决方案,但在 Python 中,其实有一种更简单的做法——预绑定方法模式。

预绑定方法模式(prebound method pattern)是一种将对象方法绑定为函数的模式。要实现该模式,第一步就是完全删掉 AppConfig 里的单例设计模式。因为在 Python 里,实现单例压根儿不用这么麻烦,我们有一个随手可得的单例对象——模块(module)。

当你在 Python 中执行 import 语句导入模块时,无论 import 执行了多少次,每个被导入的模块在内存中只会存在一份(保存在 sys.modules 中)。因此,要实现单例模式,只需在模块里创建一个全局对象即可:

class AppConfig:
“”“程序配置类,使用单例模式”“”

def __init__(self): ➊
    # 省略:从外部配置文件读取配置
    ...

_config = AppConfig() ➋

1
2
3
4
5
❶ 完全删掉单例模式的相关代码,只实现 __init__ 方法

❷ _config 就是我们的“单例 AppConfig 对象”,它以下划线开头命名,表明自己是一个私有全局变量,以免其他人直接操作

下一步,为了给其他模块提供好用的 API,我们需要将单例对象 _config 的公有方法绑定到 config 模块上:

file: project/config.py

_config = Config()

get_database_conf = _config.get_database
reload_config = _config.reload

1
之后,其他模块就可以像调用普通函数一样操作应用配置对象了:

from project.config import get_databse_conf

db_conf = get_databse_conf()
reload_config()

通过“预绑定方法模式”,我们既避免了复杂的单例设计模式,又有了更易使用的函数 API,可谓一举两得。