深入剖析Python的单例模式实现 一、引言 二、单例的应用

一、引言

单例模式是一种常见的设计模式,它限制一个类只能生成一个实例。在Python开发中,我们该如何实现单例模式呢?本文将通过一个简单的例子,使用Python的元类来实现一个线程安全的单例类,并比较说明使用装饰器实现单例的优劣。

单例模式看起来简单,但是想要做到完全线程安全并支持子类继承,还有一定的难度。本文将从单例模式的概念和应用场景开始,一步步分析线程安全的单例类该如何设计,加锁来保证线程安全等。

二、单例的应用场景

  1. 系统只需要一个实例对象,比如配置类、日志、工具类等。使用单例可以直接保证全局只存在一个实例。
  2. 控制资源访问,比如一个硬件资源只允许一个进程访问,或者打印机只允许一个任务执行打印操作。
  3. 频繁创建和销毁实例会带来较高的系统开销,使用单例可以减少内存占用和性能消耗。比如任务池、连接池等。
  4. 想确保一个类只有一个可见的实例,并提供一个全局访问点,如线程池、缓存、会话对象等。
  5. 当类状态需要频繁保存和恢复时,可以让类成为单例,避免每次获取实例后都要恢复状态的操作。
  6. 在面向对象中,如果有状态共享的需求,可以将共享状态和逻辑封装在一个单例类中。
  7. 单例可以简化代码,从而降低维护成本。在不需要多个实例的情况下,单例可以消除判断逻辑。

如下是一些伪代码的单例DEMO

配置类

配置类信息在程序运行期间仅需要一个实例,使用单例模式可以保证全局唯一:

1
2
3
4
5
6
python复制代码class Config(metaclass=SingletonMetaCls):
def __init__(self):
self.config = {'timeout':100, 'port':8000}

# 访问配置
print(Config().config)

2. 日志类

日志类也只需要一个实例输出日志信息即可:

1
2
3
4
5
6
7
8
9
python复制代码class Logger(metaclass=SingletonMetaCls):
def __init__(self):
print("初始化logger")

def log(self, msg):
print(f"log: {msg}")

# 使用
Logger().log("测试日志")

任务池

控制任务池的资源个数,只初始化指定数量的连接:

1
2
3
4
5
6
python复制代码class TaskPool(metaclass=SingletonMetaCls):
def __init__(self, size=10):
print(f"初始化{size}个任务到池中")

# 初始化一个10大小的任务池
pool = TaskPool()

总之,任何只需要一个实例、不保存状态的工具/帮助类,你需要限制实例个数的场景,都可以考虑使用单例模式实现。

三、单例的实现

重写 __new__ 方法

Python 的 new 对象不像java等其他语言一样,一些初学者可能会误认为 __init__ 方法是构造对象,实则不是,__init__方法是初始化对象属性,而__new__ 方法才是真正构造类实例对象。因此我们可以通过 重写__new__方法 并在方法内部添加判断逻辑,来限制一个类只创建一个实例。

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
30
31
32
python复制代码class Singleton(object):
_instance = None

def __new__(cls, *args, **kwargs):
# 重写 __new__ 实现单例
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance

def __init__(self):
# 初始化实例属性
self.demo_name = "Singleton Demo"


s1 = Singleton()
s2 = Singleton()

print("s1 demo_name", s1.demo_name)
print("s2 demo_name", s2.demo_name)
print("s1", s1)
print("s2", s2)
print("s1 is s2", s1 is s2)
print("s1 == s2", s1 == s2)


>>> out
s1 demo_name Singleton Demo
s2 demo_name Singleton Demo
s1 <__main__.Singleton object at 0x1051dcb80>
s2 <__main__.Singleton object at 0x1051dcb80>
s1 is s2 True
s1 == s2 True

通过重写 __new__ 实现单例,就可以实现最简单的单例模式。可以发现创建的对象的内存地址都是一样的。

上面的实现模式属于懒汉模式,还有一种叫做饿汉模式。先简单介绍下这两种模式概念。

懒汉模式

懒汉模式是等到需要才创建实例,比如:

一个游戏需要读取玩家存档数据的类,如果玩家没有存档,就不需要创建该类的实例,等玩家第一次存档时再实例化该类,读取并保存游戏状态。这种情况下使用懒汉模式更合适,不会提前占用内存资源。

饿汉模式

饿汉模式是提前创建实例,比如:

一个数据库连接池类,系统启动时就需要初始化一个指定大小的连接池,以备后续使用。这里需要饿汉模式提前创建并准备好数据库连接池,否则后面需要数据库连接时会出现延迟。

  • 懒汉是按需创建,节省资源
  • 饿汉是准备实例,避免后续延迟

饿汉代码实现

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
python复制代码#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 单例DEMO }
# @Date: 2023/08/22 09:27


class BaseSingleton(object):
_instance = None

@classmethod
def instance(cls):
if not cls._instance:
cls._instance = cls()
return cls._instance


class Singleton(BaseSingleton):
# _instance = Singleton() 这是错误的语法

def __new__(cls, *args, **kwargs):
if not cls._instance:
BaseSingleton.instance()
return cls._instance

def __init__(self):
# 初始化实例属性
self.demo_name = "Singleton Demo"


s1 = Singleton.instance()
s2 = Singleton.instance()
s3 = Singleton()
s4 = Singleton()

print("s1", s1)
print("s2", s2)
print("s3", s3)
print("s4", s4)
print("s1 is s2", s1 is s2)
print("s2 is s3", s2 is s3)
print("s3 is s4", s3 is s4)


>>> out
s1 <__main__.BaseSingleton object at 0x1051dcf70>
s2 <__main__.BaseSingleton object at 0x1051dcf70>
s3 <__main__.BaseSingleton object at 0x1051dcf70>
s4 <__main__.BaseSingleton object at 0x1051dcf70>
s1 is s2 True
s2 is s3 True
s3 is s4 True

python的饿汉模式,不能直接在类中构造自身对象,如下是错误的写法

1
2
python复制代码class Singleton(BaseSingleton):
_instance = Singleton() # 这是错误的语法

因此这里通过添加一个静态方法 instance() 来实现饿汉单例模式,但感觉有点不太像,就是要在new 对象前先通过 instance() 方法初始化下对象实例,到后面在其他模块使用已经存在的实例即可。但有时候就是想,instance() 是单例,Singleton() 这种new 对象不是,那就不要重写 __new__ 方法即可。因为有时候new对象想重新初始化属性。

虽然通过重写 __new__ 方法,实现了单例模式,但不够完善,在并发的情况下还是会创建多个实例,属于线程不安全,因此还是需要改造下,这里先展示并发问题,具体改造看下面装饰器的写法。

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
python复制代码class Singleton(object):
_instance = None

def __new__(cls, *args, **kwargs):
# 重写 __new__ 实现单例
if not cls._instance:
print("new instance")
cls._instance = super().__new__(cls, *args, **kwargs)

return cls._instance

def __init__(self):
# 初始化实例属性
self.demo_name = "Singleton Demo"


def create_obj(num):
s = Singleton()
print(f"s{num}", s)


# 多线程测试并发
with ThreadPoolExecutor() as pool:
for i in range(3, 10):
pool.submit(create_obj, i)

测试效果,这个要多运行几遍才有概率复现

可以发现,多线程的打印是凌乱的,但已经可以证明有2个线程创建了两实例对象 new instance,对象的内存地址也不一样。这是由于 if not cls._instance: 操作是非原子性操作的导致的并发问题。

装饰器写法

重写 __new__ 方法还是比较容易懂,但不太方便使用,每个类都要重写这个方法就很麻烦,逻辑都是一样的,因此我上面抽了一个 BaseSingleton 类来做,通过继承来复用代码。还有一种方式就是通过装饰器来实现单例,把共用的逻辑放到装饰器中做,然后再处理下并发问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码def singleton(cls_obj):
"""单例装饰器"""
_instance_dic = {}
_instance_lock = threading.Lock()

@functools.wraps(cls_obj)
def wrapper(*args, **kwargs):
if cls_obj in _instance_dic:
# 实例字典中存在则直接返回
return _instance_dic.get(cls_obj)

with _instance_lock: # 互斥锁,防止多线程竞争,导致创建多实例
if cls_obj not in _instance_dic:
# 实例字典中没有,则创建对象实例,存入字典中
_instance_dic[cls_obj] = cls_obj(*args, **kwargs)
return _instance_dic.get(cls_obj)

return wrapper

由于 if cls_obj not in _instance_dic 判断是非原子性操作故而会引发多线程并发问题。

它大致会转换成以下字节码指令执行:

  1. 加载_instance_dic到栈顶
1
2
python复制代码Copy code
LOAD_GLOBAL 0 (_instance_dic)
  1. 加载cls_obj到栈顶
1
2
python复制代码Copy code
LOAD_FAST 0 (cls_obj)
  1. 调用__contains__方法检查是否在字典中
1
2
python复制代码Copy code
CONTAINS_OP
  1. 根据返回值进行跳转
1
2
python复制代码Copy code
POP_JUMP_IF_FALSE <target>

如果cls_obj不在_instance_dic中,就会跳转到target位置,也就是if块内的代码。

可以看到校验是否在字典中是在多个指令中完成,不是一个原子操作。

在多线程环境下,如果多个线程同时执行到这里,都可能会通过校验,然后创建实例添加到字典中,从而导致线程不安全。

故而在装饰器中通过线程的互斥锁来解决并发问题,然后通过字典来判断是否存在类的实例对象,存在直接返回,不存在创建对象实例存入字典中来达到单例的效果。

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
30
31
32
33
34
python复制代码@singleton
class Foo(object):

def __init__(self):
self.bar = "bar"


@singleton
class Demo(object):

def __init__(self):
self.demo_name = "singleton_demo"


f1 = Foo()
f2 = Foo()
print(f1)
print(f2)
print("f1 is f2", f1 is f2)

d1 = Demo()
d2 = Demo()
print(d1)
print(d2)
print("d1 is d2", d1 is d2)


>>> out
<__main__.Foo object at 0x102f56c70>
<__main__.Foo object at 0x102f56c70>
f1 is f2 True
<__main__.Demo object at 0x102f56d60>
<__main__.Demo object at 0x102f56d60>
d1 is d2 True

并发安全验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码
@singleton
class Foo(object):

def __init__(self):
self.bar = "bar"

def two_bar(self):
return self.bar * 2


def create_obj(num):
foo = Foo()
print(f"foo{num}", foo)
return foo


with ThreadPoolExecutor() as pool:
for i in range(10):
pool.submit(create_obj, i)

ok,对象实例都是 <main.Foo object at 0x1016ed700>, 大家可以多运行几次,加了锁不会出现多个实例对象了。

这里发现被装饰的类都实现了单例模式,接下来我们一探究竟,在装饰器内部打印些东西,看看其工作原理。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
python复制代码
def singleton(cls_obj):
"""单例装饰器"""
print("cls_obj", cls_obj)

_instance_dic = {}
_instance_lock = threading.Lock()

@functools.wraps(cls_obj)
def wrapper(*args, **kwargs):
if cls_obj in _instance_dic:
# 实例字典中存在则直接返回
return _instance_dic.get(cls_obj)

with _instance_lock: # 互斥锁,防止多线程竞争,导致创建多实例
if cls_obj not in _instance_dic:
# 实例字典中没有,则创建对象实例,存入字典中
_instance_dic[cls_obj] = cls_obj(*args, **kwargs)

print("_instance_dic", _instance_dic, "\n")

return _instance_dic.get(cls_obj)

return wrapper


@singleton
class Foo(object):

def __init__(self):
self.bar = "bar"


@singleton
class Demo(object):

def __init__(self):
self.demo_name = "singleton_demo"


f1 = Foo()
f2 = Foo()
print(f1)
print(f2)
print("f1 is f2", f1 is f2)

d1 = Demo()
d2 = Demo()
print(d1)
print(d2)
print("d1 is d2", d1 is d2)

模块在初始化的时候,其实就会把类初始化形成类对象,注意不是类的实例对象。

  • 装饰器的原理就是python解释器识别到 @singleton 的语法糖时自动把类对象的引用传递给 singleton 装饰器函数
  • 此时装饰器会返回一个新的函数对象(wrapper)出去,把类对象重新赋值了
+ **Foo = singleton(Foo) = wrapper**
+ **Demo = singleton(Demo) = wrapper**
  • 到创建对象实例时,Foo() 实则变成了是调用函数 wrapper() 来创建对象
  • 然后每个类都维护了一份 _instance = {} 实例字典,来确保这个类创建的对象只有一份
+ Key 是类对象,eg:Foo、Demo
+ Value 是类的实例对象,eg:Foo(),Demo()

可能大家会不了解类对象的概念,可以先看看我这篇文章 你真的了解Python中的类class? - 掘金 然后再回来看就更容易看懂了。

但装饰器实现的单例模式装饰方便、代码简洁,但是破坏了类的类型,把类变成了函数,导致编写代码的时候没有提示,也不知道有什么属性与方法,所以实际使用起来及其不方便。

接下来就是引出另一种写法,元类实现单例。

元类写法

元类是一种非常晦涩的知识点,一般场景都用不上,但知道元类的原理,后面需要用到时,可以帮助你更好的抽象与封装。

元类就是创建 类对象的类,type 就是元类

可以先了解下元类的知识点:追溯Python类的鼻祖——元类 - 掘金

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
python复制代码#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 元类模块 }
# @Date: 2022/11/26 16:43
import threading


class SingletonMetaCls(type):
""" 单例元类 """
_instance_lock = threading.Lock()

def __init__(cls, *args, **kwargs):
cls._instance = None
super().__init__(*args, **kwargs)

def __call__(cls, *args, **kwargs):
if cls._instance:
# 存在实例对象直接返回,减少锁竞争,提高性能
return cls._instance

with cls._instance_lock:
if not cls._instance:
cls._instance = super().__call__(*args, **kwargs)
return cls._instance

使用单例元类进行单例的封装会比装饰器的更好一些,装饰器封装的单例,再实际使用的过程中不太方便,IDE一些开发工具不知道这个类有什么属性,元类就不会,继承也可以实现单例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码class Foo(metaclass=SingletonMetaCls):

def __init__(self):
self.bar = "bar"

def tow_bar(self):
return self.bar * 2


foo1 = Foo()
foo2 = Foo()
print("foo1 is foo2", foo1 is foo2)

>>> out
foo1 is foo2 True

继承案例

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
30
31
32
python复制代码class Foo(metaclass=SingletonMetaCls):

def __init__(self):
self.bar = "bar"

def tow_bar(self):
return self.bar * 2


foo1 = Foo()
foo2 = Foo()
print("foo1 is foo2", foo1 is foo2)
print("foo1 is foo2", foo1 is foo2)


class Demo(Foo):

def __init__(self):
self.bar = "demo_bar"


demo1 = Demo()
demo2 = Demo()
print("demo1 is demo2", demo1 is demo2)
print("demo2 two_bar", demo2.tow_bar())


>>> out
foo1 is foo2 True
foo2 two_bar barbar
demo1 is demo2 True
demo2 two_bar demo_bardemo_bar

元类实现原理

  • 加载Foo、Demo等类时,发现指定了元类 metaclass=SingletonMetaCls, 则会让指定的元类来帮助创建类对象
  • 此时 SingletonMetaCls 会调用__init__ 来创建类对象,然后通过super() 让 type 来创建类对象
+ type(类名, 父类元组, 类属性字典)
+ 并动态加了个 cls.\_instance 属性
  • Foo()、Demo(),创建实例对象时,是Foo、Demo类对象触发了(),所以调用 call() 魔法属性来构造对象实例,存到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
30
31
32
33
python复制代码import threading


class SingletonMetaCls(type):
""" 单例元类 """
_instance_lock = threading.Lock()

def __init__(cls, *args, **kwargs):
cls._instance = None
print("SingletonMetaCls __init__", cls)
print("args", args)
print("kwargs", kwargs)
super().__init__(*args, **kwargs)

def _init_instance(cls, *args, **kwargs):
if cls._instance:
# 存在实例对象直接返回,减少锁竞争,提高性能
print("cls._instance", cls._instance)
return cls._instance

with cls._instance_lock:
if cls._instance is None:
cls._instance = super().__call__(*args, **kwargs)
return cls._instance

def __call__(cls, *args, **kwargs):
print("SingletonMetaCls __call__ cur cls", cls)
instance = cls._init_instance()
reinit = kwargs.get("reinit", True)
if reinit:
# 默认都重新初始化单例对象属性
instance.__init__(*args, **kwargs)
return 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
python复制代码from py_tools.meta_cls import SingletonMetaCls


class Foo(metaclass=SingletonMetaCls):

def __init__(self):
print("Foo __init__")
self.bar = "bar"

def __new__(cls, *args, **kwargs):
print("Foo __new__")
return super().__new__(cls, *args, **kwargs)

def tow_bar(self):
return self.bar * 2


# foo1 = Foo()
# foo2 = Foo()
# print("foo1 is foo2", foo1 is foo2)
# print("foo2 two_bar", foo2.tow_bar())


class Demo(Foo):

def __init__(self):
self.bar = "demo_bar"

模块加载时就会走元类的__init__

1
2
3
4
5
6
7
python复制代码SingletonMetaCls __init__ <class '__main__.Foo'>
args ('Foo', (), {'__module__': '__main__', '__qualname__': 'Foo', '__init__': <function Foo.__init__ at 0x100eb0310>, '__new__': <function Foo.__new__ at 0x100ed53a0>, 'tow_bar': <function Foo.tow_bar at 0x100ed5430>, '__classcell__': <cell at 0x100eaafd0: SingletonMetaCls object at 0x1217193b0>})
kwargs {}

SingletonMetaCls __init__ <class '__main__.Demo'>
args ('Demo', (<class '__main__.Foo'>,), {'__module__': '__main__', '__qualname__': 'Demo', '__init__': <function Demo.__init__ at 0x100ed54c0>})
kwargs {}

看看new对象的时候的打印信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码class Foo(metaclass=SingletonMetaCls):

def __init__(self):
print("Foo __init__")
self.bar = "bar"

def __new__(cls, *args, **kwargs):
print("Foo __new__")
return super().__new__(cls, *args, **kwargs)

def tow_bar(self):
return self.bar * 2


foo1 = Foo()
foo2 = Foo()
print("foo1 is foo2", foo1 is foo2)

输出信息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码SingletonMetaCls __init__ <class '__main__.Foo'>
args ('Foo', (), {'__module__': '__main__', '__qualname__': 'Foo', '__init__': <function Foo.__init__ at 0x104998550>, '__new__': <function Foo.__new__ at 0x1049d5550>, 'tow_bar': <function Foo.tow_bar at 0x1049d55e0>, '__classcell__': <cell at 0x104991fd0: SingletonMetaCls object at 0x12f626fb0>})
kwargs {}

SingletonMetaCls __call__ cur cls <class '__main__.Foo'>
Foo __new__
Foo __init__
Foo __init__

SingletonMetaCls __call__ cur cls <class '__main__.Foo'>
cls._instance <__main__.Foo object at 0x1049b3f70>
Foo __init__
foo1 is foo2 True

可以发现跟我上面的说的一致,这里引出了好多魔法属性来验证,可以先看看 Python中的魔法属性 - 掘金

四、总结

  • 类重写 new 易懂,但每个类都要重写太冗余了
+ 故抽出 BaseSingleton 基类,复用逻辑通过 instance() 来实现单例(**推荐**)
+ 如果要构造实例属性会有点不太方便
  • 装饰器写法也是复用了创建单例的逻辑,装饰起来方便、简洁
+ 但实际使用装饰过的类不方便,没有类属性提示
  • 元类的写法会有点难与绕,实际使用起来方便,多继承也实现了单例(推荐
+ 使用起来和平常使用类没有区别
+ 还可以通过**reinit**参数来控制是否重新初始化实例对象属性
  • 通过线程的互斥锁来解决并发问题
+ 双重判断来减少锁竞争,提高性能
  • 当然还有其他的方式实现单例,例如通过Python的模块导入,来保证只会创建一个实例

五、源代码

有些细节,我没有展开讲,大家可以下载源代码,亲自实践下。

HuiDBK/py-tools: 打造 Python 开发常用的工具,让Coding变得更简单 (github.com)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%