0%

Python3 单例模式:确保类仅有一个实例

Python 单例模式:一个类只能有一个实例,到底怎么实现?

写程序的时候,有些东西全局只能有一份——配置管理器、日志记录器、数据库连接池。如果每个地方都 new 一个,资源浪费不说,状态还不一致。

单例模式就是干这个的:确保一个类在整个程序里只有一个实例,所有人都用它。

单例要解决的问题,先看一个反例

1
2
3
4
5
6
7
8
9
class Config:
def __init__(self):
self.settings = {}

config1 = Config()
config1.settings["theme"] = "dark"

config2 = Config()
print(config2.settings) # {},白存了

config1config2 是两个不同的对象,配置存到 config1 里,config2 拿不到。这就是问题——配置这种全局只有一份的东西,不应该被重复创建。

单例模式要的效果:

1
2
3
config1 = Config()
config2 = Config()
print(config1 is config2) # True,同一个对象

最简单:模块级单例,利用 Python 的导入机制

Python 的模块在第一次被导入时执行,之后再导入直接用已经加载好的。这是最简单、最 Pythonic 的方式。

config.py

1
2
3
4
5
6
7
8
9
10
11
12
class Config:
def __init__(self):
self.settings = {}

def set(self, key, value):
self.settings[key] = value

def get(self, key):
return self.settings.get(key)

# 直接在模块里创建一个实例
config = Config()

使用:

1
2
3
4
from config import config

config.set("theme", "dark")
# 任何其他地方 from config import config 拿到的都是同一个对象

优点: 代码量最少,不需要任何技巧,Python 天然支持。

缺点: 不能延迟初始化(导入的时候实例就创建了),灵活性稍差。

适合场景: 日志记录器、配置管理、工具类等简单场景。大部分时候这就够了,不需要更复杂的实现。

_new_方式:控制实例的创建过程

如果你确实需要”调用类的构造函数来控制单例”,用 __new__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singleton:
_instance = None

def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self, value):
# 注意:__init__ 每次都会被调用,需要加标志避免重复初始化
if not hasattr(self, "_initialized"):
self.value = value
self._initialized = True

s1 = Singleton("first")
s2 = Singleton("second")

s1 is s2 # True
s1.value # "first",第二次初始化没生效

关键点:

  • __new____init__ 之前调用,负责”造出对象”
  • __new__ 返回什么,__init__ 就初始化什么
  • 判断 _instance is None 决定是否创建,否则返回已有对象
  • __init__ 每次都会被调用,需要用 hasattr_initialized 标志阻止重复初始化

适合场景: 类需要正常的构造函数调用方式(MyClass()),不想用模块级变量。

装饰器方式:不改类代码,加个装饰器就行

如果你不想修改类的内部实现,可以加个装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def singleton(cls):
instances = {}

def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper

@singleton
class DatabaseConnection:
def __init__(self, url):
self.url = url
print(f"连接:{url}")

db1 = DatabaseConnection("mysql://localhost")
db2 = DatabaseConnection("mysql://localhost") # 不会新建
db1 is db2 # True

优点: 不侵入类的内部代码,可以给多个类复用。

缺点: 装饰器包装后,类的类型信息可能会变(type(db1) 不再是 DatabaseConnection),需要额外处理。

适合场景: 你想给现有的类加单例行为,但不想改它的代码。

元类方式:从根源上控制类的行为

元类是”创建类的类”,控制类的实例化过程。用元类实现单例是最彻底的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SingletonMeta(type):
_instances = {}

def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]

class Config(metaclass=SingletonMeta):
def __init__(self):
self.settings = {}

c1 = Config()
c2 = Config()
c1 is c2 # True

原理: 类的 __call__ 方法控制着 MyClass() 这个调用的行为。元类重写了 __call__,在每次”创建实例”时判断是否已经存在。

优点: 最彻底,支持继承(子类也会自动单例),行为最接近”类本身保证单例”。

缺点: 元类对初学者不太友好,理解成本高。

适合场景: 框架级别的基础类,或者你需要确保所有子类也都是单例的时候。

线程安全问题

上面的 __new__ 和元类实现,在多线程环境下可能出问题——两个线程同时判断 _instance is None,都认为该创建,结果造出两个实例。

加锁解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import threading

class ThreadSafeSingleton:
_instance = None
_lock = threading.Lock()

def __new__(cls, *args, **kwargs):
# 第一重检查(无锁,快速路径)
if cls._instance is None:
with cls._lock:
# 第二重检查(有锁,确保只创建一次)
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

这叫”双重检查锁”,常见于多线程单例实现。

但要注意: 如果你的应用不是多线程的,不需要加锁,徒增开销。

一个完整的例子:数据库连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import threading

class DatabaseConnection:
_instance = None
_lock = threading.Lock()

def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
if not hasattr(self, "_initialized"):
self.connections = []
self._initialized = True

def get_connection(self):
print("获取一个数据库连接")
return "connection"

def release_connection(self, conn):
print("释放连接")

# 使用
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True

欢迎关注我的其它发布渠道