查看: 77|回复: 1

Python单例装饰器实现持久化:支持Pydantic与自动恢复实例

[复制链接]
发表于 半小时前 | 显示全部楼层 |阅读模式
在开发中,单例模式常用于管理全局状态(如配置管理器、缓存服务)。但普通单例在程序重启后会丢失状态。本文介绍一个支持持久化的单例装饰器,可自动将实例保存到JSON文件,下次启动时恢复,且兼容Pydantic等禁止动态添加属性的库。

核心代码实现

以下是完整实现,包含三种调用方式和Pydantic兼容处理:
  1. import os
  2. import types
  3. from functools import wraps
  4. from typing import TypeVar
  5. import jsonpickle
  6. from pydantic import BaseModel
  7. T = TypeVar('T')
  8. def single(cls_or_filename: T | str = None, *, filename: str = None) -> T:
  9.     """单例装饰器,支持持久化。
  10.    
  11.     用法:
  12.         @single                # 默认文件名 类名.json
  13.         @single()
  14.         @single('file.json')
  15.         @single(filename='file.json')
  16.     """
  17.     if isinstance(cls_or_filename, type):
  18.         # @single 直接装饰类
  19.         fname = cls_or_filename.__qualname__ + '.json'
  20.         return _decorate(cls_or_filename, fname)
  21.     else:
  22.         fname = cls_or_filename if cls_or_filename is not None else filename
  23.         def decorator(cls: T):
  24.             return _decorate(cls, fname)
  25.         return decorator
  26. def _decorate(cls: T, fname: str) -> T:
  27.     _instance: T = None
  28.     # 用于jsonpickle解码时的类映射
  29.     class_fullname = cls.__module__ + '.' + cls.__qualname__
  30.     class_mapping = {class_fullname: cls}
  31.     @wraps(cls)
  32.     def wrapper(*args, **kwargs) -> T:
  33.         nonlocal _instance
  34.         if _instance is None:
  35.             if fname is not None and os.path.exists(fname):
  36.                 with open(fname, 'r', encoding='utf-8') as f:
  37.                     _instance = jsonpickle.decode(f.read(), classes=class_mapping)
  38.             else:
  39.                 _instance = cls(*args, **kwargs)
  40.             # 动态添加 save 方法
  41.             if not hasattr(_instance, 'save'):
  42.                 def save(self, filename: str = None):
  43.                     save_path = filename or fname
  44.                     if save_path is None:
  45.                         raise ValueError(
  46.                             "No filename specified. Either provide a filename to save() "
  47.                             "or use the decorator with a filename."
  48.                         )
  49.                     with open(save_path, 'w', encoding='utf-8') as f:
  50.                         f.write(jsonpickle.encode(self))
  51.                 # 绕过Pydantic限制
  52.                 object.__setattr__(_instance, 'save', types.MethodType(save, _instance))
  53.         return _instance
  54.     return wrapper
复制代码

三个关键实现细节

一、支持三种装饰语法

- @single                  # 无参数,默认文件名为 类名.json
- @single()               # 同上
- @single('b.json')       # 指定持久化文件
- @single(filename='b.json')

实现原理:检查第一个参数类型。如果是类(type),表示直接装饰;否则返回一个decorator函数等待接收类。注意当有文件名参数时,必须返回decorator,此时cls尚未传入。

二、jsonpickle 需要 class_mapping

编码时jsonpickle会保存类的全限定名(如 __main__.MyClass)。解码时必须通过classes参数告诉它全限定名对应的类,否则无法反序列化。代码中:
  1. class_mapping = {class_fullname: cls}
  2. _instance = jsonpickle.decode(f.read(), classes=class_mapping)
复制代码

这样解码就能正确重建实例。

三、object.__setattr__ 绕过 Pydantic 限制

Pydantic的BaseModel在__setattr__中进行了验证,会拒绝动态添加的属性(如save方法)。直接赋值 _instance.save = ... 会触发ValidationError。使用object.__setattr__绕过Pydantic的验证,直接写入实例的__dict__,使之正常工作。该技巧也适用于其他禁止动态属性的库。

使用示例

基本用法(无参数)
  1. @single
  2. class A:
  3.     def __init__(self, x: int = 0):
  4.         self.x = x
  5. a = A(5)
  6. print(type(a))          # <class '__main__.A'>
  7. print(a.x)              # 5
  8. a.save()                # 保存到 A.json
复制代码

指定持久化文件
  1. @single('b.json')
  2. class B:
  3.     def __init__(self, y: str = ''):
  4.         self.y = y
  5. b = B('hello')
  6. b.save()                # 保存到 b.json
复制代码

配合 Pydantic 使用
  1. @single
  2. class Task(BaseModel):
  3.     id: str = 'id'
  4.     label: str = 'label'
  5.     children: list['Task'] = []
  6. t = Task()
  7. print(t.id)             # id
  8. print(t.label)          # label
  9. t.save()                # 正常保存,Pydantic不会拦截
复制代码

总结与适用场景

该装饰器自动为类添加save()方法,支持三种调用语法。通过jsonpickle实现序列化,利用class_mapping保证解码正确。object.__setattr__技巧让装饰器兼容Pydantic等框架。适用于配置管理器(启动恢复,修改后持久化)、缓存服务(重启不丢失)、轻量级状态管理(无需数据库)等场景。
回复

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: Python单例装饰器实现持久化:支持Pydantic与自动恢复实例

这个实现很实用,尤其是对 Pydantic 的兼容处理很巧妙。我之前也遇到过用 pickle 或 jsonpickle 恢复实例时动态添加方法被 Pydantic 拦截的问题,用 `object.__setattr__` 绕过是个好思路。 有个小疑问:`jsonpickle` 序列化时会不会把 `save` 方法也存进去?如果实例里有自定义方法或有状态的闭包,反序列化后这些方法还在吗?另外在分布式或高并发场景下,会不会有文件锁或线程安全的问题?不过对于单进程单线程的配置管理类来说,这套方案已经很完整了,感谢分享。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-16 09:57 , Processed in 0.022578 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部