Python实现支持持久化的单例装饰器

 更新时间:2026年06月16日 08:45:57   作者:huzhongqiang  
本文介绍了一个支持持久化的单例装饰器single,核心特点在于兼容三种装饰语法,支持JSON文件恢复实例等内容,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下

前言

之前提到过装饰器可以实现单例模式。

这次更进一步,带来一个支持持久化的单例装饰器

  • 免参数装饰 @single:实例自动获得 save() 方法
  • 带文件名装饰 @single('file.json'):启动时从文件恢复,调用 save() 默认保存到该文件
  • Pydantic 兼容:即使 BaseModel 禁止动态添加字段也能工作

一、核心代码

import os
import types
from functools import wraps
from typing import TypeVar
import jsonpickle
from pydantic import BaseModel


T = TypeVar('T')

def single(cls_or_filename: T|str = None, *,filename:str=None) -> T:
    """单例装饰器:
        - @single / @single() :仅单例,实例获得 save(filename) 方法
        - @single('path/file.json') / @single(filename='path/file.json') :
        指定持久化文件,首次创建时若文件存在则从中恢复;实例获得 save() 方法
        """
    if isinstance(cls_or_filename, type):
        # @single 直接装饰类
        fname = cls_or_filename.__qualname__ + '.json'  # 默认文件名是类名 + .json
        return _decorate(cls_or_filename, fname)  # type: ignore[return-value]
    else:
        fname = cls_or_filename if cls_or_filename is not None else filename
        # 注意:不能直接'return _decorate(cls, fname)',因为此时还没有传入cls
        def decorator(cls: T):
            return _decorate(cls, fname)
        return decorator

def _decorate(cls: T, fname: str) -> T:
    _instance: T = None  # 单例实例
    # 用于 jsonpickle 解码时的类映射,避免递归调用 wrapper
    class_fullname = cls.__module__ + '.' + cls.__qualname__
    class_mapping = {class_fullname: cls}
    @wraps(cls)
    def wrapper(*args, **kwargs) -> T:
        nonlocal _instance
        if _instance is None:
            if fname is not None and os.path.exists(fname):
                with open(fname, 'r', encoding='utf-8') as f:
                    _instance = jsonpickle.decode(f.read(), classes=class_mapping)
            else:
                _instance = cls(*args, **kwargs)
            # 动态添加 save 方法
            if not hasattr(_instance, 'save'):
                def save(self, filename: str = None):
                    save_path = filename or fname
                    if save_path is None:
                        raise ValueError(
                            "No filename specified. Either provide a filename to save() "
                            "or use the decorator with a filename."
                        )
                    with open(save_path, 'w', encoding='utf-8') as f:
                        f.write(jsonpickle.encode(self))
                # 为了防止pydantic禁止动态添加字段,这里使用object.__setattr__强制添加
                object.__setattr__(_instance, 'save', types.MethodType(save, _instance))
        return _instance  # type: ignore[return-value]
    return wrapper

二、三个实现细节

支持三种装饰语法

@single                    # 无参数,默认文件名是 类名.json
@single()                 # 同上
@single('b.json')         # 指定文件名
@single(filename='b.json') # 关键字参数指定文件名

实现原理是根据第一个参数的类型判断

if isinstance(cls_or_filename, type):
    # @single 直接装饰类,此时第一个参数就是类本身
    return _decorate(cls_or_filename, fname)
else:
    # @single('file.json') 两段式调用,先返回一个 decorator,再注入 cls
    def decorator(cls: T):
        return _decorate(cls, fname)
    return decorator

关键:第二段 @single('b.json') 返回的是 decorator(cls) 而不是直接 decorate(cls, fname),因为此时 cls 还没有传入。

jsonpickle 需要 class_mapping

class_fullname = cls.__module__ + '.' + cls.__qualname__
class_mapping = {class_fullname: cls}
_instance = jsonpickle.decode(f.read(), classes=class_mapping)

如果不传 classes=class_mapping,jsonpickle 解码时会遇到问题:

  • 编码时保存的是类的全限定名(如 __main__.B
  • 解码时 jsonpickle 不知道这个全限定名对应哪个类
  • 没有正确的类映射,就无法反序列化

class_mapping 把类的全限定名映射回原始类,确保解码正确。

object.setattr绕过 Pydantic 限制

object.__setattr__(_instance, 'save', types.MethodType(save, _instance))

问题:Pydantic 的 BaseModel 默认禁止动态添加字段,直接 _instance.save = ... 会触发验证错误。

解决:通过 object.__setattr__ 绕过 Pydantic 的 __setattr__ 拦截,直接写到实例的 __dict__ 里。同样的技巧也适用于其他禁止动态属性的库。

三、使用示例

基本用法

@single
class A:
    def __init__(self, x: int = 0):
        self.x = x

a = A(5)
print(type(a))    # <class '__main__.A'>,类型依然是 A
print(a.x)        # 5
a.save()          # 保存到 A.json

指定持久化文件

@single('b.json')
class B:
    def __init__(self, y: str = ''):
        self.y = y

b = B('hello')
b.save()  # 保存到 b.json

配合 Pydantic 使用

@single
class Task(BaseModel):
    id: str = 'id'
    label: str = 'label'
    children: list['Task'] = []

t = Task()
print(t.id)     # id
print(t.label)  # label
t.save()        # 正常保存,不会被 Pydantic 拦截

四、总结

要点回顾

细节说明
三种语法无参数 / @single('f.json') / @single(filename='f.json')
两段式返回有文件名时必须返回 decorator(cls),因为 cls 还没传进来
class_mappingjsonpickle 解码时需要映射类的全限定名,避免无法反序列化
object.setattr绕过 Pydantic 等禁止动态添加字段的限制

适用场景

  • 配置管理器:启动时恢复配置,修改后自动持久化
  • 缓存服务:重启后缓存不丢失
  • 轻量级状态管理:不需要数据库

以上就是Python实现支持持久化的单例装饰器的详细内容,更多关于Python单例装饰器的资料请关注脚本之家其它相关文章!

相关文章

  • Python打包文件执行报错:ModuleNotFoundError: No module named ‘pymssql‘的解决方法

    Python打包文件执行报错:ModuleNotFoundError: No module 

    这篇文章给大家介绍了Python打包文件执行报错:ModuleNotFoundError: No module named ‘pymssql‘的解决方法,如果有遇到相同问题的朋友可以参考阅读一下本文
    2023-10-10
  • 浅谈Scrapy网络爬虫框架的工作原理和数据采集

    浅谈Scrapy网络爬虫框架的工作原理和数据采集

    在python爬虫中:requests + selenium 可以解决目前90%的爬虫需求,难道scrapy 是解决剩下的10%的吗?显然不是。scrapy框架是为了让我们的爬虫更强大、更高效。接下来我们一起学习一下它吧。
    2019-02-02
  • python numpy中setdiff1d的用法说明

    python numpy中setdiff1d的用法说明

    这篇文章主要介绍了python numpy中setdiff1d的用法说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Python通过队列实现进程间通信详情

    Python通过队列实现进程间通信详情

    这篇文章主要介绍了Python通过队列实现进程间通信详情文章通过提出问题:在多进程中,每个进程之间是什么关系展开主题相关内容,感兴趣的朋友可以参考一下
    2022-06-06
  • Python SQLite3简介

    Python SQLite3简介

    这篇文章主要为大家详细介绍了Python SQLite3的简单介绍以及使用方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-02-02
  • CentOS下Python3的安装及创建虚拟环境的方法

    CentOS下Python3的安装及创建虚拟环境的方法

    这篇文章主要介绍了CentOS下Python3的安装及创建虚拟环境的方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-11-11
  • Pytorch实现张量的创建与使用方法

    Pytorch实现张量的创建与使用方法

    本文主要介绍了Pytorch实现张量创建使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-08-08
  • Python实现专业级字符串清理技术的完全指南

    Python实现专业级字符串清理技术的完全指南

    在数据处理领域,超过80%的时间都花在数据清洗上,而字符串净化是其中最关键的一环,本文将系统解析Python字符串净化技术体系,希望对大家有所帮助
    2025-08-08
  • 深入探讨Python中的异常处理及最佳实践

    深入探讨Python中的异常处理及最佳实践

    异常处理是编写健壮、可靠和易于调试的Python代码中不可或缺的一部分,在本文中,我们将深入探讨Python中的异常处理机制,并分享一些最佳实践和代码示例,希望对大家有所帮助
    2025-11-11
  • 如何在Python项目中引入日志

    如何在Python项目中引入日志

    在开发一些大型项目的时候,都会使用日志来记录项目运行时产生的信息,以备出错时定位分析和从日志信息中提取数据统计分析等。在 Python 中使用 logging 内置模块即可对项目进行日志的配置。
    2021-05-05

最新评论