Python 中单例模式实现的几种方式

本贴最后更新于 327 天前,其中的信息可能已经渤澥桑田

前言

单例模式是一种常用的创建型设计模式,它保证一个类只有一个实例,并提供一个全局的访问点。

单例模式在自动化中的应用常用场景有(来自ChatGPT回答):

  1. 管理测试资源:自动化测试通常需要使用各种资源,例如数据库连接、网络连接、文件系统等。使用单例模式可以确保这些资源在整个测试过程中只有一个实例存在,避免资源的重复创建和销毁,提高测试的效率。
  2. 共享测试数据:在自动化测试中,可能需要在不同的测试用例之间共享测试数据,例如测试数据的准备和清理过程。使用单例模式可以确保这些测试数据在整个测试过程中只有一个实例存在,避免数据不一致或冗余的问题。
  3. 控制测试环境:自动化测试通常需要在特定的测试环境中运行,例如测试服务器、模拟器等。使用单例模式可以限制测试环境的实例化次数,确保整个测试过程中只有一个测试环境存在,避免资源的浪费和冲突。
  4. 简化测试代码:使用单例模式可以提供一个全局访问点,使得可以在任何需要时方便地访问测试资源和状态。这样可以简化测试代码,避免在不同测试用例中传递实例或重复创建实例的麻烦。
  5. 控制测试并发:在并发测试中,如果多个测试用例同时访问相同的资源或状态,可能会导致数据竞争和不确定的结果。使用单例模式可以确保资源和状态的线程安全访问,控制测试并发,避免并发问题的发生。

总的来说,自动化测试需要单例模式是为了管理测试资源、共享测试数据、控制测试环境、简化测试代码和控制测试并发。单例模式可以提高测试效率、减少资源浪费,并确保测试的可靠性和一致性。然而,在使用单例模式时,需要注意线程安全性和对测试代码的影响,确保合理使用单例模式的同时不引入新的问题。

几种单例模式的实现方式

重写__new__()方法

在Python中:

  1. __new__() 方法是一个用于创建实例的静态方法
  2. 使用 类名() 创建对象时,Python 的解释器 首先 会 调用 __new__ 方法为对象 分配空间
  3. __new__是一个 由 object 基类提供的 内置的静态方法,主要作用有两个:
    1. 在内存中为对象 分配空间
    2. 返回 对象的引用(地址)

通过重写这个方法,在创建实例的时候一个类只返回一个实例就可以了。


class Demo:
    _instance = None

    def __init__(self):
        print("执行初始化")

    def __del__(self):
        print(f'{self}已经被删除')

    def __str__(self):
        return "Demo对象"

    __repr__ = __str__

    def __new__(cls, *args, **kwargs):
        # 判断类属性是否已经被赋值:
        # (如果未被赋值,调用父类的方法,为第一个对象分配空间,然后返回这个引用)
        # (如果已经赋值,直接返回它保存的单例引用)
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance


if __name__ == '__main__':
    d1 = Demo()
    d2 = Demo()
    print(d1 is d2)
执行初始化
执行初始化
True
Demo对象已经被删除

通过运行结果可以发现初始化方法被调用了两次,这是因为对 __new__ 方法改造之后,每次都会得到 第一次被创建对象的引用, 但是初始化方法还会被再次调用

那么,如何只执行一次初始化操作呢?

解决办法:

  1. 定义一个类属性 init_flag 标记是否 执行过初始化动作 ,初始值为 False
  2. init 方法中,判断 init_flag,如果为 False 就执行初始化动作
  3. 然后将 init_flag 设置为 True
  4. 这样,再次 自动 调用 __init__ 方法时,初始化动作就不会被再次执行
class Demo:
    # 记录第一个被创建对象的引用
    _instance = None
    # 记录是否执行过初始化动作
    _init_flag = False

    def __init__(self):
        if Demo._init_flag:
            return
        print("执行初始化")
        Demo._init_flag = True

    def __del__(self):
        print(f'{self}已经被删除')

    def __str__(self):
        return "Demo对象"

    __repr__ = __str__

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance


if __name__ == '__main__':
    d1 = Demo()
    d2 = Demo()
    print(d1 is d2)

# 运行结果
执行初始化 - ----只执行了一次
True
Demo对象已经被删除

使用装饰器

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 MySingleton:
    pass


if __name__ == '__main__':
    s1 = MySingleton()
    s2 = MySingleton()
    print(id(s1))
    print(id(s2))
    print(s1 is s2)
  
# 运行结果
1852731129520
1852731129520
True

使用__call__()方法

类对象创建实例对象时一定会调用__call__方法,因此重写元类的__call__方法,保证在调用__call__时只创建一个实例即可。

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __call__(self):
        return self._instance


if __name__ == '__main__':
    s1 = Singleton()
    s2 = Singleton()
    print(id(s1))
    print(id(s2))
    print(s1 is s2)  # True

使用元类

与使用__call__方法差不多

class SingletonMetaclass(type):
    _instance = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instance:
            cls._instance[cls] = super().__call__(*args, **kwargs)
            return cls._instance[cls]
        return cls._instance[cls]


class Singleton(metaclass=SingletonMetaclass):
    pass


if __name__ == '__main__':
    s1 = Singleton()
    s2 = Singleton()
    print(s1 is s2)   # True

自动化测试简单应用:日志记录器

from pathlib import Path
from loguru import logger
from common.path_config import PathConfig


class SingletonLogger:
    _instance = None
    _init_flag = False

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls, *args, **kwargs)
            return cls._instance
        return cls._instance

    def __init__(self, log_path=PathConfig.LOG_PATH, init_flag=_init_flag):
        if init_flag:
            return
        self.log_path = log_path
        self.logger = self.create_log()
        SingletonLogger.init_flag = True

    def create_log(self):
        Path.mkdir(self.log_path, parents=True, exist_ok=True)
        self.logs = str(Path(self.log_path, "{time:YYYY_MM_DD}.log"))
        log_config = {
            "level": "DEBUG",
            "rotation": "500MB",
            "retention": "7 days",
            "encoding": "utf-8",
            "enqueue": True
        }
        logger.add(self.logs, **log_config)
        return logger

    def info(self, msg):
        return self.logger.info(msg)

    def warning(self, msg):
        return self.logger.warning(msg)

    def exception(self, msg):
        return self.logger.exception(msg)


if __name__ == "__main__":
    log1 = SingletonLogger()
    log1.info("这是一条日志信息")
    try:
        log1.info("这是一条记录信息")
        print(1 / 0)
    except ZeroDivisionError:
        log1.exception("0不能作为被除数")

    log2 = SingletonLogger()
    print(log1 is log2)

运行结果

2023-11-26 21:53:47.338 | INFO     | __main__:info:41 - 这是一条日志信息
2023-11-26 21:53:47.339 | INFO     | __main__:info:41 - 这是一条记录信息
2023-11-26 21:53:47.339 | ERROR    | __main__:exception:47 - 0不能作为被除数
Traceback (most recent call last):

> File "D:\project\demoproject\demo1.py", line 55, in <module>
    print(1 / 0)

ZeroDivisionError: division by zero
True

每天学一点huaji

1 操作
xiaodingdang 在 2023-12-08 18:00:38 更新了该帖
回帖
请输入回帖内容 ...