ITPub博客

首页 > 数据库 > 数据库开发技术 > 你知道单例模式吗?Python如何实现?

你知道单例模式吗?Python如何实现?

原创 数据库开发技术 作者:jackzhang 时间:2024-07-06 22:43:56 0 删除 编辑

如果你准备从事Python后端开发,那么在面试中可能会被问到如何实现单例模式(Singleton Pattern)。

1. 什么是单例模式?

单例模式(Singleton Pattern)是一种软件设计模式,顾名思义,旨在确保一个类只有一个实例,并提供一个全局访问点来访问这个示例。单例模式通过控制实例化过程,确保一个类在整个程序生命周期中只有一个对象实例存在。

简单来说,一个单例类有且仅有一个对象实例。如果我们尝试再次实例化,实际上最终得到的却是同一个实例。

2. 单例模式的作用

  • 节约系统开销:控制一个类只有一个实例,可以有效节省系统资源。
  • 实例共享:提供一个全局访问点,使得不同部分的代码可以共享同一个实例。
  • 延迟实例化:单例模式可以实现延迟实例化,即只有在第一次使用时才会创建实例。

3. 为什么需要单例模式呢?

单例模式在以下场景比较有用:

  • 配置类:在应用程序中,配置通常是全局唯一的。通过单例模式,可以确保配置类只有一个实例,这样不同的模块就可以共享同一配置。
  • 日志类:日志记录通常是全局的,通过单例模式,可以确保所有的日志记录操作都使用同一个实例,方便管理和输出。
  • 资源管理:如数据库连接池、线程池等资源,使用单例模式可以避免资源的频繁创建和销毁,提高系统性能。
  • 缓存:缓存数据通常需要全局访问,通过单例模式可以确保缓存管理的唯一性和一致性。
  • 设备驱动:对于一些硬件设备的控制类,如打印机驱动、音频管理器等,使用单例模式可以避免硬件设备的多次初始化。

4. 单例模式的实现

单例模式可以通过多种方式实现,以下是一些常见的实现方式:

4.1 懒汉式单例(Lazy Singleton)

这种单例表示实例在第一次使用时才创建:

class LazySingleton:
    __instance = None
    def __new__(cls, *args, **kwargs):
        # if cls.__instance does not exist, instantiate it.
        if cls.__instance is None:
            print('Creating LazySingleton instance')
            cls.__instance = super(LazySingleton, cls).__new__(cls, *args, **kwargs)
        
        # else, simply return the existing instance
        return cls.__instance
    
if __name__ == '__main__':
    print('Before using LazySingleton')
    lazy1 = LazySingleton() # It should print "Creating LazySingleton instance" at here.
    lazy2 = LazySingleton() # It does not create instance again.
    print(f'layzy1 is lazy2: {lazy1 is lazy2}')

从输出中可以看到虽然我们创建两个实例,但是“Creating LazySingleton instance”只打印了一次,说明只创建了一个实例。

这里,我们重写了 __new__ 魔术方法,它定义了类实例化的方式。

  • 如果 cls.__instance 不存在,那么我们就创建它,并且对其进行实例化。
  • 如果 cls.__instance 已存在,那么我们只需要简单地返回现有示例即可。
  • 通过这种方法,我们就可以实现只有当 cls.__instance 为 None 时才会创建该实例。

比如,在下面的示例中,我们使用懒汉式单例模式实现一个配置管理类,确保配置在整个程序中只有一个实例。

class ConfigurationManager:
    __instance = None
    def __new__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super(ConfigurationManager, cls).__new__(cls, *args, **kwargs)
            # initiate configuration
            cls.__instance.config = {}
        return cls.__instance
    
    def get(self, key):
        return self.config.get(key)
    
    def set(self, key, value):
        self.config[key] = value
        
if __name__ == '__main__':
    config1 = ConfigurationManager()
    config2 = ConfigurationManager()
    config1.set('database', 'MySQL')
    print(f"config2: {config2.get('database')}")
    print(f'config1 is config2: {config1 is config2}')
    print(f'config1: {config1}')
    print(f'config2: {config2}')

在这个示例中,我们尝试创建 ConfigurationManager 的两个实例 config1 和 config2,并且用 config1 设置实例信息,但是我们用 config2 获取到实例信息与 config1 设置的一样。此外,当我们输出这两个实例时,他们都有相同的哈希地址 0x00000151832FFE10。

这就意味着 config1 和 config2 也许是两个不同的变量,但它们实际上指向同一个对象,即 ConfigurationManager 的单实例。

4.2 饿汉式单例(Eager Singleton)

这种单例表示实例在类加载时就创建:

class EagerSingleton:
    __instance = None
    
    def __new__(cls, *args, **kwargs):
        raise RuntimeError('Call get_instance() instead!')
    
    @classmethod
    def get_instance(cls):
        return cls.__instance
    
    @classmethod
    def _create_instance(cls):
        print('Creating EagerSingleton instance')
        cls.__instance = super(EagerSingleton, cls).__new__(cls)
    
if __name__ == '__main__':
    # instance created intermediately at class load time.
    EagerSingleton._create_instance()
    
    print('Before using EagerSingleton')
    eager1 = EagerSingleton.get_instance()  # does not create instance again.
    eager2 = EagerSingleton.get_instance()
    print(f'eager1 is eager2: {eager1 is eager2}')
    print(f'eager1: {eager1}')
    print(f'eager2: {eager2}')

_create_instance 方法用于在类加载时创建实例。从输出中可以看到,在类加载的时候就创建了实例,后面再次实例化实际上是调用 get_instance 方法获取现有实例。

在下面这个示例中,我们使用饿汉式单例创建了一个日志记录类,实现日志的记录和管理。

class Logger:
    __instance = None
    
    def __new__(cls, *args, **kwargs):
        raise RuntimeError('Call get_instance() instead!')
    
    @classmethod
    def get_instance(cls):
        return cls.__instance
    
    @classmethod
    def _create_instance(cls):
        cls.__instance = super(Logger, cls).__new__(cls)
        cls.__instance.logs = []
    
    def log(self, message):
        self.logs.append(message)
        
    def get_logs(self):
        return self.logs
    
if __name__ == '__main__':
    Logger._create_instance()
    logger1 = Logger.get_instance()
    logger2 = Logger.get_instance()
    logger1.log('Log message 1')
    logger1.log('Log message 2')
    print(f'Log info: {logger1.get_logs()}')
    print(f'logger1 is logger2: {logger1 is logger2}')

4.3 通过装饰器实现单例模式

def singleton(cls):
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

@singleton
class DecratorSingleton:
    pass

这里,我们通过装饰器将 DecratorSingleton 变成了一个单例类。

下面我们通过一个数据库连接池的示例来展现装饰器实现单例模式的用法:

def singleton(cls):
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            print(f'Creating a new instance of {cls.__name__}')
            instances[cls] = cls(*args, **kwargs)
        else:
            print(f'Using existing instance of {cls.__name__}')
        return instances[cls]
    
    return get_instance

@singleton
class DatabaseConnectionPool:
    def __init__(self):
        self.connections = []
        
    def get_connection(self):
        if self.connections:
            return self.connections.pop()
        else:
            # create new connection
            return 'New Connection'
        
    def return_connection(self, connection):
        self.connections.append(connection)
        
if __name__ == '__main__':
    pool1 = DatabaseConnectionPool()
    pool2 = DatabaseConnectionPool()
    conn1 = pool1.get_connection()
    pool2.return_connection(conn1)
    print(pool1.get_connection())
    print(f'pool1 is pool2: {pool1 is pool2}')

在我们的代码中使用了 DatabaseConnectionPool() 的方式来创建实例,但实际上是调用了装饰器返回的 get_instance 方法,而不是直接实例化 DatabaseConnectionPool 类。这样,DatabaseConnectionPool() 就变成了一个单例类。

4.4 通过元类实现单例模式

通过元类控制类的实例化过程,从而实现单例模式:

class SingletonMeta(type):
    __instance = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls.__instance:
            cls.__instance[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
        return cls.__instance[cls]
    
class Singleton(metaclass=SingletonMeta):
    pass

下面是一个设备驱动管理器的示例,我们通过元类实现单例模式,确保设备驱动管理器只有一个实例。

class SingletonMeta(type):
    __instance = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls.__instance:
            cls.__instance[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
        return cls.__instance[cls]
    
class DeviceDriverManager(metaclass=SingletonMeta):
    def __init__(self):
        # Initiate device driver
        self.devices = {}
        
    def register_device(self, device_id, device):
        self.devices[device_id] = device
        
    def get_device(self, device_id):
        return self.devices.get(device_id)
    
if __name__ == '__main__':
    manager1 = DeviceDriverManager()
    manager2 = DeviceDriverManager()
    manager1.register_device('d001', 'Printer Device')
    print(manager2.get_device('d001'))  # Output: Pinter Device
    print(f'manger1 is manager2: {manager1 is manager2}')

这里,当我们通过 DeviceDriverManager() 的方式实例化时,实际上是调用了元类的 __call__ 方法,元类控制了类的实例只会被创建一个。

5. 单例模式的优缺点

优点:

  • 控制实例数量,节省系统资源。
  • 提供全局访问点,方便访问和管理。
  • 实现延迟实例化,减少初始化开销。

缺点:

  • 可能引发多线程问题,需要处理线程安全。
  • 增加全局状态,可能导致系统难以维护和测试。
  • 难以扩展,由于单例类通常不能被继承,限制了其灵活性。

单例模式也有个不可避免的潜在漏洞,那就是如果用户真的想打破你的单例类的话,他们可以重写单例类的 __new__ 方法。

class Singleton:
    __instance = None
    
    def __new__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__new__(cls)
            
        return cls
    
if __name__ == '__main__':
    Singleton.__new__ = object.__new__  # break singleton pattern
    s1 = Singleton()
    s2 = Singleton()
    s3 = Singleton()
    print(s1)
    print(s2)
    print(s3)

从输出中可以看出三个实例对象的哈希地址都不相同,此时,在我们的有意破坏之后,Singleton 类就再也不满足单例模式了。

目前并没有合适的方法阻止这种情况的发生。Python 似乎赋予了用户强大的实用功能,同时也给予了人们破坏代码的能力。

所以,我们只能寄希望于我们的用户不知道如何搞破坏吧!😄😄😄


推荐阅读👇👇👇

机器学习基础:今天的主角—逻辑回归(Logistics Regression)!献给正在入门机器学习的你!

机器学习基础:一文搞懂简单线性回归的原理,新手也能看懂!

机器学习基础:一文搞懂“二分类之王”—支持向量机(SVM)背后的数学原理

Python自动化:适合新手练习的5个有趣又实用的Python脚本,帮你快速掌握编程技能!拿走不谢!

万字长文:数据科学中 25 个最重要的数学概念(收藏!!!)

- END -

🌟 喜欢这篇文章吗?给个赞支持一下吧!🌟

如果你发现今天的内容对你有帮助,不妨点个赞并关注我,第一时间接收更多关于Python、数据分析和机器学习的深入分析和最新趋势。我们一起探索数据的力量,解锁AI的潜能。你的每一次互动都是对我的极大鼓励,也是我们共同成长的动力!

👇👇👇 关注并开启星标提醒,让知识与技能的火花不断在你的职业道路上燃烧吧!

原文作者:数据派探险家;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2017-12-19

  • 博文量
    119
  • 访问量
    55980