说说 python 中单例模式的实现

单例模式设计要注意的细节可真不少(。_。)

0%

简单介绍

单例模式是一种非常常用的软件设计模式,它定义是单例对象的类只能允许一个实例存在

单例模式有两种经典的实现方式饿汉模式懒汉模式, 接下来以它们为开端来介绍单例模式.

懒汉模式

顾名思义, 创建对象时比较懒, 到需要用到时才去创建.

使用__new__类方法实现, 每次实例化类时会判断是否已有类属性_instance(里面存的是已经实例化的类对象), 有的话直接返回, 没有就创建新实例.

class Singleton:

def __init__(self):
pass

# 类的构造方法, 会在__init__方法前执行
def __new__(cls):
if not hasattr(cls, '_instance'):
cls._instance = super().__new__(cls)
return cls._instance

if __name__ == "__main__":
S1 = Singleton()
S2 = Singleton()
print(id(S1))
print(id(S2))

输出结果

139789593957088
139789593957088

但这样存在一个问题, 如果同时实例化多个类, if not hasattr(cls, '_instance')语句有可能同时成立, 这会造成创建多个新的实例, 比如在多线程操作下.

import time
from threading import Thread


class Singleton:

def __init__(self):
pass

# 类的构造方法, 会在__init__方法前执行
def __new__(cls):
if not hasattr(cls, '_instance'):
time.sleep(1) # 模拟程序延迟
cls._instance = super().__new__(cls)
return cls._instance

def foo():

S = Singleton()
print(id(S))

if __name__ == "__main__":
thread_array = [Thread(target=foo) for _ in range(2)]
[t.start() for t in thread_array]

输出结果

139858525528848
139858525530288

这是因为多线程下的操作并不是原子性的, 解决方法也很简单, 加上互斥锁将__new__方法里的操作转变成原子性就好了.

import time
from threading import Lock, Thread


class Singleton:
lock = Lock()

def __init__(self):
pass

# 类的构造方法, 会在__init__方法前执行
def __new__(cls):
with cls.lock: # 加锁, 使操作转为原子性
if not hasattr(cls, '_instance'):
time.sleep(1) # 模拟程序延迟
cls._instance = super().__new__(cls)
return cls._instance

def foo():
S = Singleton()
print(id(S))

if __name__ == "__main__":
thread_array = [Thread(target=foo) for _ in range(2)]
[t.start() for t in thread_array]

输出结果

140572949160720
140572949160720

这样就没问题了吧? 不, 我们还需要对__init__函数做处理, 比方说以下的情况, 每次实例化虽不会创建新实例, 当会执行一遍初始化里的代码, 这是不久单例了个寂寞吗?

import time
from threading import Lock, Thread


class Singleton:
lock = Lock()

def __init__(self):
print('初始化中')
# code...
print('初始化完毕')

# 类的构造方法, 会在__init__方法前执行
def __new__(cls):
with cls.lock: # 加锁, 使操作转为原子性
if not hasattr(cls, '_instance'):
time.sleep(1) # 模拟程序延迟
cls._instance = super().__new__(cls)
return cls._instance

def foo():
S = Singleton()
print(id(S))

if __name__ == "__main__":
thread_array = [Thread(target=foo) for _ in range(2)]
[t.start() for t in thread_array]

输出结果

初始化中
初始化完毕
140424166351632
初始化中
初始化完毕
140424166351632

为了优化这一问题, 在__init__里加个判断就行了.

def __init__(self):
if hasattr(self, '_init'):
return
self._init = True

print('初始化中')
# code...
print('初始化完毕')

到了这一步完整的懒汉模式就已经实现了, 但是, 还有个细节可以优化, 这里引入双重检查锁的概念.

双重检查锁

双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。

这样会在保证性能的同时又保证单例. (小声BB: python还需要考虑性能? 不, 这是对细节的把控!)

def __new__(cls):
if not hasattr(cls, '_instance'): # 加锁前判断
with cls.lock:
if not hasattr(cls, '_instance'): # 加锁后判断
cls._instance = super().__new__(cls)
return cls._instance

最终代码

from threading import Lock, Thread


class Singleton:
lock = Lock()

def __init__(self):
if hasattr(self, '_init'):
return
self._init = True

def __new__(cls):
if not hasattr(cls, '_instance'):
with cls.lock:
if not hasattr(cls, '_instance'):
cls._instance = super().__new__(cls)
return cls._instance

饿汉模式

提前创建好对象, 要用到就直接使用, 不会被'饿'死.

python 和 Java 不同, 没办法在类中实例化本身, 所以传统的实现方式行不通, 这里我认为比较合理的实现应该是:

class Singleton:

def __init__(self):
pass

Singleton = Singleton()

直接进行实例化, 然后调用实例化对象, 欸, 这不就是正常的调用类的方式吗? 确实, 但它却满足了饿汉模式实现的条件, 权当了解即可.

两者的优缺点

  • 饿汉模式:优点是没有线程安全的问题,缺点是浪费内存空间。
  • 懒汉模式:优点是没有内存空间浪费的问题,缺点是如果控制不好,实际上不是单例的。
------------ 已触及底线了 感谢您的阅读 ------------
  • 本文作者: OWQ
  • 本文链接: https://www.owq.world/34b94b2a/
  • 版权声明: 本站所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处( ̄︶ ̄)↗