你真的了解Python的单例模式吗?

最近在用Python单例模式的时候遇到一些问题, 还是自己太年轻了, 在这里总结一下我在使用这个设计模式的时候的坑.
前言(单例模式简介)
单例模式提供了这样一个机制,即确保类有且只有一个特定类型的对象,并提供全局访问点。因此,单例模式通常用于下列情形,例如日志记录或数据库操作、打印机后台处理程序,以及其他程序——该程序运行过程中只能生成一个实例,以避免对同一资源产生相互冲突的请求。例如,我们可能希望使用一个数据库对象对数据库进行操作,以维护数据的一致性;或者希望使用一个日志类的对象,将多项服务的日志信息按照顺序转储到一个特定的日志文件中。
简单来说, 单例模式可以总结为下面三个要点:
- 确保类有且只有一个对象被创建。
- 为对象提供一个访问点,以使程序可以全局访问该对象。
- 控制共享资源的并行访问。
下文中, 我们将会用Python来实现单例模式, 不做特殊说明, 下文中所有的代码均基于Python 3.6.8实现.
经典的单例模式
代码如下所示:
class Singleton:
def __new__(cls, *args, **kwargs):
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
def test():
s1 = Singleton()
s2 = Singleton()
print(f"s1: {s1}\ns2: {s2}")
我们来运行代码查看一下运行结果.

可以发现上述代码满足如下两个要求:
- 只允许Singleton类生成一个实例。
- 如果已经有一个实例了,会重复提供同一个对象。
懒汉实例化版单例模式
单例模式的用例之一就是懒汉式实例化。例如,在导入模块的时候,我们可能会无意中创建一个对象,但当时根本用不到它。懒汉式实例化能够确保在实际需要时才创建对象。所以,懒汉式实例化是一种节约资源并仅在需要时才创建它们的方式。下面我们来看一下代码:
class LazySingleton:
__instance = None
def __init__(self):
if not LazySingleton.__instance:
print('Called __init__ method...')
else:
print("Instance already created @: ", self.get_instance())
@classmethod
def get_instance(cls):
if not cls.__instance:
cls.__instance = LazySingleton()
return cls.__instance
def test():
s1 = LazySingleton()
print("Created Object", LazySingleton.get_instance())
s2 = LazySingleton()
同样来看一下代码的运行结果,

利用元类实现单例模式
为什么会考虑到这么写, 因为如果我想对某个类实现单例模式, 我要复制上面的一堆代码, 这显现不够Pythonic, 因此考虑一下使用元类来实现. 有人可能发问了, 这为什么不用继承来实现, 写一个基类, 其他子类继承他不就好了吗? 悄悄的说一句, 如果用继承的话是有坑的, 下面首先来看一下继承方法为什么不可以.

来看一个简单的例子, 还是使用前面所说的简单单例.
class Singleton:
def __new__(cls, *args, **kwargs):
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
class FirstChildSingleton(Singleton):
pass
class SecondChildSingleton(Singleton):
pass
def test():
s1 = Singleton()
s2 = Singleton()
print(f"s1: {s1}\ns2: {s2}")
first_child = FirstChildSingleton()
second_child = SecondChildSingleton()
print(f"first child: {first_child}\nsecond child: {second_child}")
打印一下结果, 可以发现,

这里所有的子类也是同一个对象, 这显然是不行的.
什么是元类
让我们先来了解一下元类。元类是一个类的类,这意味着该类是它的元类的实例。使用元类,程序员有机会从预定义的Python类创建自己类型的类。
在Python中,一切皆对象。如果我们说a=1,则type(a)返回<type'int'>,这意味着a是int类型。但是,type(int)返回<type'type'>,这表明存在一个元类,因为int是type类型的类。
具体有关元类的知识, 在本文中不多介绍了, 不是这一篇文章的重点.
单例实现
首先还是老套路, 先来看一下代码:
class MetaSingleton(type):
def __call__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
cls._instance = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instance
class FirstChildMetaSingleton(metaclass=MetaSingleton):
pass
class SecondChildMetaSingleton(metaclass=MetaSingleton):
pass
def test():
s1 = FirstChildMetaSingleton()
s2 = SecondChildMetaSingleton()
print(f"s1: {s1}\ns2: {s2}")
我们可以发现, 这样的话, 实例化出来的单例就不是同一个了.

线程安全的单例
你以为写个元类, 这样就可以解决一切问题吗? 哈哈, 太天真了, 上述的写法实际上是非线程安全的, 我们来看一个例子.

先来看一下代码,
class SimpleSingleton(metaclass=MetaSingleton):
def __init__(self):
time.sleep(1)
def create_in_thread(name):
s = SimpleSingleton()
print(f'{name}: {s}')
def test():
t1 = threading.Thread(target=create_in_thread, args=("first thread",))
t2 = threading.Thread(target=create_in_thread, args=("second thread",))
t1.start()
t2.start()
t1.join()
t2.join()

从上图中可以看到, 这样会创建两个单例, 也就是说这样代码是非线程安全的, 我太难了, 下面来看一个线程安全的单例吧.
class MetaThreadSecuritySingleton(type):
_instance_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
with MetaThreadSecuritySingleton._instance_lock:
if not hasattr(cls, "_instance"):
cls._instance = super(MetaThreadSecuritySingleton, cls).__call__(*args, **kwargs)
return cls._instance
class SimpleThreadSecuritySingleton(metaclass=MetaThreadSecuritySingleton):
def __init__(self):
time.sleep(1)
def create_in_thread(name):
s = SimpleThreadSecuritySingleton()
print(f'{name}: {s}')
def test():
t1 = threading.Thread(target=create_in_thread, args=("first thread",))
t2 = threading.Thread(target=create_in_thread, args=("second thread",))
t1.start()
t2.start()
t1.join()
t2.join()

最终这个单例终于变得线程安全了, 生活中处处充满着惊喜, 微笑着面对它, 奥利给.


本文探讨了Python中的单例模式,包括其概念、经典实现、懒汉实例化以及利用元类实现。通过示例代码解释了单例模式在多线程环境中的问题和线程安全的解决方案,帮助读者深入理解Python单例模式的应用。
591

被折叠的 条评论
为什么被折叠?



