解析Python中的单例

发布 : 2019-10-04 分类 : basics 浏览 : --

单例是软件设计模式中比较常用的模式,也是最方便实现的一种模式。正是因此,其实现方式也是最能够体现一个人对python基础知识的掌握情况的标尺

实现方式

每一种单例的实现方式都会有很多可以联系到的基础知识

以下根据该单例存储的位置范畴由大到小依次说明

1. 全局存储

俗称懒汉模式

1
2
3
4
class _Singleton(object):
pass

Singleton = _Singleton() # 想要的单例

当需要使用这个单例时,直接使用 Singlton 实例即可。最简单的情况是在直接使用这个单例,无论是在同一段代码还是以包的形式引用。稍微有点区别的是,以包的形式引用的话,还需要知道 import 这个语法引入变量是单例的,保证了 Singleton 变量地址不发生变化。更妙的是,这段代码不会产生竞争条件,也就是说不会有多线程环境下多次实例化的情况(正常使用的话)。

然而,如果使用 reload 重新加载这个包含单例的包时,将会产生新的实例。还有另一点,一般不希望在全局存在过多变量,可能在无意中造成不需要的单例的创建,换句话说就是,这种单例不容易由自己决定在什么时候创建,除非你很清楚自己在做什么。最后,这种模式下如果文档中说明不清晰,或者跟其他开发人员交流不够明白,有可能导致手动再创建一个实例,因为 _Singleton 这个类是可以手动再实例化的。

虽然全局存储的形式看上去问题挺多,但这种方式其实揭示了单例模式的通行做法—— 保存实例

其他创建单例的形式无非是让这个过程变得更加安全和健壮了而已

2. 静态变量存储

将实例存储为静态变量的方式将该实例的访问权限缩小为通过类及其实例,相对全局而言提升了一层抽象。据我观察,这也是实现形式最多的一种方式,也是隐藏问题最多的一种。

1
2
3
4
5
6
7
8
9
10
11
12
from threading import Lock

class Singleton(object):
_lock = Lock()

def __new__(cls, *args, **kwargs):
if hasattr(cls, "__instance__"):
return cls.__instance__
with Singleton._lock:
if not hasattr(cls, "__instance__"):
cls.__instance__ = object.__new__(cls)
return cls.__instance__

这里主要利用了 __new__ 方法控制实例的创建过程,但是有几个需要注意的地方

1) 新式类

这个问题存在于python2.7及以下

也就是说这个类必须是 object 的子类,否则 __new__ 方法将失效。说白了,__new__ 方法是 object 给的

2) 静态变量名

静态变量的名字不能是私有变量,也就是说不能偷懒给全局变量命名为 __instance

因为会被重写为 _Singleton__instance ,也就是说 hasattr(cls, "__instance__") 永远都是 False ,因此每次创建实例时都会重建实例

为了避免这种隐含问题,减少动态属性的使用,一般也会这么写:

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

class Singleton(object):
_lock = Lock()
_instance = None

def __new__(cls, *args, **kwargs):
if cls._instance:
return cls._instance
with Singleton._lock:
if not cls._instance:
cls._instance = object.__new__(cls)
return cls._instance

3) 初始化方法重复执行

__init__ 方法作为初始化方法,在每次创建实例完成后都会默认执行,也就是在 __new__ 之后自动执行,所以如果有什么只希望在单例创建时执行的动作一定不能直接放在 __init__

所以这里也就有了其他写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from threading import Lock

class Singleton(object):
_lock = Lock()
_instance = None

def __new__(cls, *args, **kwargs):
if cls._instance:
return cls._instance
with Singleton._lock:
if not cls._instance:
cls._instance = cls.get_instance()
return cls._instance

@classmethod
def get_instance(cls): # 作用为取代 __init__ 兼顾创建实例
# do something heavy
import time
time.sleep(1)
return object.__new__(cls)

以及这种写法的退化写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from threading import Lock

class Singleton(object):
_lock = Lock()
_instance = None

@classmethod
def get_instance(cls):
if cls._instance:
return cls._instance
with cls._lock:
if not cls._instance:
# do something heavy
import time
time.sleep(1)
return object.__new__(cls)

instance = Singleton.get_instance() # 想要的单例需要这样生成

其实就是使用类方法代替了 __new__ 方法,存储了一个实例到静态变量 _instance 。当然,比较不爽的地方就是单例的获取只能通过调用类方法 get_instance 获得

4) 单例模式不可继承

这里强调是模式不可继承,也就是说父类是单例模式,子类不会不经修饰就成为天然单例模式。而且对于简单的继承,可能会出大问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from threading import Lock

class Singleton(object):
_lock = Lock()

def __new__(cls, *args, **kwargs):
if hasattr(cls, "__instance__"):
return cls.__instance__
with Singleton._lock:
if not hasattr(cls, "__instance__"):
cls.__instance__ = object.__new__(cls)
return cls.__instance__

class B(Singleton):
key = "B"

Singleton()
B().key # AttributeError: 'Singleton' object has no attribute 'key'

由于 BSingleton 的实例共享 __instance__ 静态变量,所以由于 Singleton 先实例化,导致 B 的实例其实还是 Singleton 的实例

正确的继承方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from threading import Lock

class Singleton(object):
_lock = Lock()

def __new__(cls, *args, **kwargs):
if hasattr(cls, "__instance__"):
return cls.__instance__
with Singleton._lock:
if not hasattr(cls, "__instance__"):
cls.__instance__ = object.__new__(cls)
return cls.__instance__

class B(Singleton):
key = "B"

def __new__(cls, *args, **kwargs):
return object.__new__(cls)

Singleton()
B().key
B().__instance__

当然,自然有一种可以继承单例模式的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from threading import Lock

class Singleton(object):
_lock = Lock()
_instance = None

@classmethod
def get_instance(cls):
if cls._instance:
return cls._instance
with cls._lock:
if not cls._instance:
# do something heavy
import time
time.sleep(1)
return object.__new__(cls)

class A(Singleton):
pass

Singleton.get_instance()
A.get_instance()

不过这种方法看起来还是有那么一点丑,所以还有一种稍微高级一点的方式,那就是 元类 。不过元类在python2和python3之间差异较大,所以是不通用的,但是原理是类似的。以下以 python2 作为例子

元类作为类的类,本质上只是给出了类创建时候的模板,最终还是依靠类本身的特性实现的。可以看看如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from threading import Lock

class SingletonMeta(type):
def __new__(cls, name, bases, attrs):
_lock = Lock()
def new(cls, *args, **kwargs):
if hasattr(cls, "__instance__"):
return cls.__instance__
with _lock:
if not hasattr(cls, "__instance__"):
cls.__instance__ = object.__new__(cls)
return cls.__instance__
attrs["__new__"] = new
return type.__new__(cls, name, bases, attrs)

class SingletonA(object):
__metaclass__ = SingletonMeta

class SingletonB(object):
__metaclass__ = SingletonMeta

上述例子本质上还是依赖 __new__ 方法实现,只是将 __new__ 方法的实现模板化,使用元类实现类的量产

3. 外部变量存储

一提到外部变量,那么就会提到闭包,一提到闭包,就会说到装饰器。由于装饰器的本质其实都是可执行对象(大多数情况下都说的是函数),所以以下以普通装饰器为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from threading import Lock
from functools import wraps

def singleton(name):
_instance = {}
_lock = Lock()

def cls_wrapper(cls):
@wraps(cls)
def init_wrapper(*args, **kwargs):
if name in _instance:
return _instance[name]
with _lock:
if name not in _instance:
_instance[name] = cls(*args, **kwargs)
return _instance[name]
return init_wrapper
return cls_wrapper

@singleton("A")
class A(object):
pass

注意:这里假设对python中的字典结构进行加key与查询key的操作是线程安全的

在这种模式下创建单例不再需要一个一个的手写,而是通过装饰器进行包装。这种方式的强大之处在于即使一个已经写好的类不是单例,也可以在外部强行转变为单例。这个例子里可能有一些地方需要特别指出(并不是无意为之)

关于装饰器的一些问题在另一篇文章中会详细整理

1) 这是一个带参数的装饰器

首先,这个参数的作用是作为实例在外部字典 _instance 中的key,其次这个key可以通过其他方式获取(也就是说可以用不带参数的装饰器实现)。当然,如果在项目中就只有一个类需要作为单例来使用,可以将key固定为 instance 类似的字符串(虽然我觉得没有多少人会这么做),甚至将整个实例占用单个外部变量(但是如果是修改外部变量会涉及到内外部作用域的问题,会用到引用传递或者 nonlocal 等)

如何让 singleton 不带参数使用?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from threading import Lock
from functools import wraps

def singleton(cls):
_instance = {}
_lock = Lock()

# name = __file__ + cls.__name__ # 在交互式shell环境中请去掉 __file__
name = repr(cls) # 这个name可能没有上面那么好看,但是好用而且通用 =.=

@wraps(cls)
def init_wrapper(*args, **kwargs):
if name in _instance:
return _instance[name]
with _lock:
if name not in _instance:
_instance[name] = cls(*args, **kwargs)
return _instance[name]
return init_wrapper

这里用到一个可能不存在的全局变量 __file__ ,是为了防止在不同python文件中存在同名的类,如果不觉得key丑的话 name = repr(cls) 也可以(其实这样的代码反而看起来很清晰=。=)

2) wraps装饰单例类

wraps的作用在于恢复被装饰类的基本元信息(对,不仅可以恢复函数的元信息,还可以恢复类的元信息),默认情况下会保留 __module__, __name__, __doc__ 三个元变量数据

3) 线程锁 Lock

_lock 锁实例在每次装饰类时实例化,在 with _lock 时控制代码片段访问序列。在一开始首先判断实例是否创建,如果已经创建,则立即返回该实例;如果没有创建,则通过加锁方式创建单例。

在锁中又判断一次该实例是否创建,是有必要的,考虑如下代码:

1
2
3
class Delay(object):
def __init__(self):
time.sleep(10)

当多个线程同时开始实例化时都会发现 _instance 中无该实例,在锁的安排下,会依次进入临界区,创建实例,但是这个过程要等待10s之久,所有的线程一定都会尝试一遍创建实例,如果不判断实例是否已经被别的线程创建了,那一定会出现多次实例化。当然,最终的确只会残留一个实例。

所以,锁 + 存在判断是为了在多线程环境下,从始至终只生成一个正确的实例。

4) 这是一个装饰器

意味着单例被使用函数创建出来,被装饰的单例类再也不是类(至少不是表面上看起来的类),而是函数,最简单的验证方式就是:

1
type(A)  # function

因为此时类已经被替换成了 cls_wrapper (带参数) 或者 init_wrapper (不带参数)了

所以丢失了原来用类来判断的能力,比如 isinstance__class__ 等等

5) 初始化仅执行一次

和上一类实现方式不一样的是,这里的单例只会执行一次初始化操作。某种意义上算是很方便的一个特性

总结

实际情况中,每个人都会根据自己的需求进行单例的实践,甚至多种方式组合使用。不过创建单例的思想一直都没有变过:常驻内存+线程安全

本文作者 : hellflame
原文链接 : https://hellflame.github.io/2019/10/04/singleton-in-python/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
留下足迹
点击通过issue留言