欢迎光临
我们一直在努力

Python中单例模式的最佳实践

  • 本文默认读者懂得单例模式的基础概念,本文侧重于单例模式的最佳实践。
  • 单例模式,说简单也简单,说难也难,只要你深挖下去,单例模式的实现其实有很多值得我们学习的地方。

使用__new__的经典实现

__init__和__new__的关系:

在python中我们最常用的是__init__,我们使用这个方法来给实例添加各种属性。__new__可能不是经常使用,我们使用__new__的情况一般只有两种:1,实现单例,2,实现不可变类型继承。从别的语言转来Python的读者,可能把它误认为构造方法。其实不然__init__只是用来在实例创建之后,给实例添加一些属性值而已,它是一个初始化方法。真正的构造方法是__new__,它是真正去申请内存创造实例的方法。__new__就像是建筑工,它会建造一个毛坯房。__init__则是一个装修工,它会给毛坯房进行装修。

所以我们在写一个单例的时候,得用构造方法,而不是初始化方法。单例就是只能建一栋房子,所以我们要控制建筑工__new__始终只能建造一间毛坯房。

class Singleton:
_instance = None

def __new__(cls, *args, **kwargs):
if cls._instance is None:
# 只有第一次为空的时候才真正分配内存,保证只建一间“毛坯房”
cls._instance = super().__new__(cls)
return cls._instance

s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # 结果是true

通过上面的代码,我们可以控制建筑工__new__只建造一间毛坯房了。但是还有一个问题,装修工__init__会不会重复装修呢?

class Singleton:
_instance = None
init_exec_num = 0 # 记录执行次数

def __new__(cls, *args, **kwargs):
if cls._instance is None:
# 只有第一次为空的时候才真正分配内存,保证只建一间“毛坯房”
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
self.__class__.init_exec_num += 1
print(f"第{self.__class__.init_exec_num}次执行")

s1 = Singleton() # 第1次执行
s2 = Singleton() # 第2次执行
s3 = Singleton() # 第3次执行

读者运行一下上述代码会发现,每一次创建实例__init__都会执行一遍。假如我们在初始化方法里面执行大量消耗资源的操作,那我们的单例不是写了一个寂寞?我们写单例的目的之一就是要节省资源嘛。

class Singleton:
_instance = None
init_exec_num = 0 # 记录执行次数

def __new__(cls, *args, **kwargs):
if cls._instance is None:
# 只有第一次为空的时候才真正分配内存,保证只建一间“毛坯房”
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
if not hasattr(self, '_initialized'):
self.__class__.init_exec_num += 1
print(f"第{self.__class__.init_exec_num}次执行")
self._initialized = True

s1 = Singleton() # 第1次执行
s2 = Singleton() # 第2次执行
s3 = Singleton() # 第3次执行

在初始化方法中引入_initialized标记是否执行过init,读者运行一下上述代码可以看到,初始化方法不再重复执行,只执行了一次。现在,我们在创建一个实例的时达到真正意义上的单例。

但是还是不太完美,这个版本是线程不安全的,要是在极端多线程情况下,实例依旧可能被创建多次!比如说在极端情况下,线程一已经创建了实例,但是还没把实例赋值给cls._instance,这时线程二进入if cls._instance is None:判断为true,开始创建实例,单例又失效了。

import threading
import time

class Singleton:
_instance = None
new_exec_num = 0 # 记录new执行次数
init_exec_num = 0 # 记录init执行次数

def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls.new_exec_num+=1
print(f"_instance为空,开始构造新实例,这是第{cls.new_exec_num}次创建")
time.sleep(0.5) # 模拟多线程环境,假设创建实例需要一点时间
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
if not hasattr(self, '_initialized'):
self.__class__.init_exec_num += 1
self._initialized = True

def task():
# 每个线程的任务就是尝试实例化 Singleton
obj = Singleton()
print(f"[线程 {threading.current_thread().name}] 拿到对象地址: {id(obj)}")

# 创建一个线程列表
threads = []

# 循环创建 5 个线程
print("— 多线程测试开始 —")
for i in range(5):
t = threading.Thread(target=task, name=f"Worker-{i+1}")
threads.append(t)
t.start() # 启动线程

# 等待所有线程结束
for t in threads:
t.join()

print("— 测试结束 —")
print(f"最终 new 执行次数: {Singleton.new_exec_num}")

读者可以自行执行一下上述的代码,会发现上面的单例实现在多线程环境下完全失效。解决方法只有一个,为构造方法加上锁,保证线程同一时刻只能有一个拿到锁并去创建实例。

import threading
import time

class Singleton:
_instance = None
new_exec_num = 0 # 记录new执行次数
init_exec_num = 0 # 记录init执行次数
_lock = threading.Lock() # 类级别的锁

def __new__(cls, *args, **kwargs):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls.new_exec_num += 1
print(f"_instance为空,开始构造新实例,这是第{cls.new_exec_num}次创建")
time.sleep(0.5) # 模拟多线程环境,假设创建实例需要一点时间
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
if not hasattr(self, '_initialized'):
self.__class__.init_exec_num += 1
self._initialized = True

def task():
# 每个线程的任务就是尝试实例化 Singleton
obj = Singleton()
print(f"[线程 {threading.current_thread().name}] 拿到对象地址: {id(obj)}")

# 创建一个线程列表
threads = []

# 循环创建 5 个线程
print("— 多线程测试开始 —")
for i in range(5):
t = threading.Thread(target=task, name=f"Worker-{i+1}")
threads.append(t)
t.start() # 启动线程

# 等待所有线程结束
for t in threads:
t.join()

print("— 测试结束 —")
print(f"最终 new 执行次数: {Singleton.new_exec_num}")

到这里我们使用__new__实现单例模式就无懈可击了,在一个进程下不可能会创建多个实例了。为了性能,这里我们使用了双重检查锁的写法,感兴趣的读者可以自行去了解一下。

利用模块导入实现单例——大道至简

使用__new__实现单例虽然很“秀”,但是一点都不Pythonic。我们要善于利用Python的特性实现我们的需求。在Python中,模块天然就是单例。

对于Python的Import机制,当你第一次执行Import导入my_module时,Python会做这些事:

  • 查找my_module.py文件
  • 执行模块中的代码
  • 缓存,把执行结果缓存到sys.module中
  • 这样后续无论你导入几次,最终只是会获取sys.module中的结果。利用sys.module的缓存机制和 Python 天然的 Import Lock,我们能在py文件中轻松实现线程安全、全局唯一的单例。除非你需要极其复杂的继承关系或严苛的懒加载控制,否则,模块即单例永远是你的第一选择。

    使用元类黑魔法实现单例

    什么是元类,如果说__new__方法的在根据类这张施工图建造毛坯房,那么元类就是这张施工图的设计师。在Python中,一切皆对象,实例是对象,类也是对象,类可以创造实例对象,元类就是创造类对象的,所有的类都是由 type这个元类创建的。当我们写MyClass() 时,Python 本质上是在调用元类的 __call__方法。所以我们就可以通过控制__call__来控制对象的生成过程。

    import threading

    class SingletonMeta(type):
    """
    这是一个线程安全的单例元类。
    """
    _instances = {}
    _lock = threading.Lock()

    def __call__(cls, *args, **kwargs):
    # 这里的 cls 指的是使用该元类的类本身(例如 Database)
    # 当我们调用 Database() 时,会触发这里的逻辑
    with cls._lock:
    if cls not in cls._instances:
    # 只有这里,才真正调用 super() 去创建实例
    # 这相当于执行了原类的 __new__ 和 __init__
    instance = super().__call__(*args, **kwargs)
    cls._instances[cls] = instance
    return cls._instances[cls]

    class Database(metaclass=SingletonMeta):
    def connect(self):
    print("连接数据库…")

    class Logger(metaclass=SingletonMeta):
    def log(self, msg):
    print(f"Log: {msg}")

    相信这时候读者就有疑问了,直接用__new__不好吗?搞这么麻烦,为了炫技?其实不然,使用元类实现单例有以下好处:

    • 解耦单例逻辑与具体业务:我们把单例的规则于具体的业务类解耦开了。业务代码可以专心地去实现业务,无需关注具体单例是怎么实现的,如果我们不需要使用单例了,直接把业务类上的元类去除就行了。
    • 提高代码复用性:如果我们使用__new__来实现单例,你会发现无论是Database还是Logger我们都需要实现一套__new__,使用元类,只需要简单地添加元类就行了。

    那么好处说完了,坏处呢?

    • 元类冲突:如果你的 Database 类本来就继承了一个类(比如 ORMModel),而那个父类也指定了自定义的元类,那么 Python 解释器就会报错,因为一个类不能同时由两个不同的元类来‘制造’。这个时候就难搞了,可能需要编写一个继承自两个元类的新元类来解决冲突。
    赞(0)
    未经允许不得转载:171主机测评 » Python中单例模式的最佳实践
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址