Python单例装饰器实现持久化:支持Pydantic与自动恢复实例
在开发中,单例模式常用于管理全局状态(如配置管理器、缓存服务)。但普通单例在程序重启后会丢失状态。本文介绍一个支持持久化的单例装饰器,可自动将实例保存到JSON文件,下次启动时恢复,且兼容Pydantic等禁止动态添加属性的库。核心代码实现
以下是完整实现,包含三种调用方式和Pydantic兼容处理:
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 # 默认文件名 类名.json
@single()
@single('file.json')
@single(filename='file.json')
"""
if isinstance(cls_or_filename, type):
# @single 直接装饰类
fname = cls_or_filename.__qualname__ + '.json'
return _decorate(cls_or_filename, fname)
else:
fname = cls_or_filename if cls_or_filename is not None else filename
def decorator(cls: T):
return _decorate(cls, fname)
return decorator
def _decorate(cls: T, fname: str) -> T:
_instance: T = None
# 用于jsonpickle解码时的类映射
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__(_instance, 'save', types.MethodType(save, _instance))
return _instance
return wrapper
三个关键实现细节
一、支持三种装饰语法
- @single # 无参数,默认文件名为 类名.json
- @single() # 同上
- @single('b.json') # 指定持久化文件
- @single(filename='b.json')
实现原理:检查第一个参数类型。如果是类(type),表示直接装饰;否则返回一个decorator函数等待接收类。注意当有文件名参数时,必须返回decorator,此时cls尚未传入。
二、jsonpickle 需要 class_mapping
编码时jsonpickle会保存类的全限定名(如 __main__.MyClass)。解码时必须通过classes参数告诉它全限定名对应的类,否则无法反序列化。代码中:
class_mapping = {class_fullname: cls}
_instance = jsonpickle.decode(f.read(), classes=class_mapping)
这样解码就能正确重建实例。
三、object.__setattr__ 绕过 Pydantic 限制
Pydantic的BaseModel在__setattr__中进行了验证,会拒绝动态添加的属性(如save方法)。直接赋值 _instance.save = ... 会触发ValidationError。使用object.__setattr__绕过Pydantic的验证,直接写入实例的__dict__,使之正常工作。该技巧也适用于其他禁止动态属性的库。
使用示例
基本用法(无参数)
@single
class A:
def __init__(self, x: int = 0):
self.x = x
a = A(5)
print(type(a)) # <class '__main__.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不会拦截
总结与适用场景
该装饰器自动为类添加save()方法,支持三种调用语法。通过jsonpickle实现序列化,利用class_mapping保证解码正确。object.__setattr__技巧让装饰器兼容Pydantic等框架。适用于配置管理器(启动恢复,修改后持久化)、缓存服务(重启不丢失)、轻量级状态管理(无需数据库)等场景。
Re: Python单例装饰器实现持久化:支持Pydantic与自动恢复实例
这个实现很实用,尤其是对 Pydantic 的兼容处理很巧妙。我之前也遇到过用 pickle 或 jsonpickle 恢复实例时动态添加方法被 Pydantic 拦截的问题,用 `object.__setattr__` 绕过是个好思路。 有个小疑问:`jsonpickle` 序列化时会不会把 `save` 方法也存进去?如果实例里有自定义方法或有状态的闭包,反序列化后这些方法还在吗?另外在分布式或高并发场景下,会不会有文件锁或线程安全的问题?不过对于单进程单线程的配置管理类来说,这套方案已经很完整了,感谢分享。Re: Python单例装饰器实现持久化:支持Pydantic与自动恢复实例
这个实现很精巧,特别是对 Pydantic 兼容的处理——用 `object.__setattr__` 绕过验证限制,既保持了单例的持久化能力,又避免破坏 Pydantic 的数据校验逻辑。三种装饰语法的重载也考虑得很周全,能适应不同的使用习惯。 有个小疑问想请教:如果保存的 JSON 文件损坏或格式不兼容,`jsonpickle.decode` 可能会抛出异常,目前似乎没有做异常处理。对于生产环境,是否需要加一个回退逻辑,比如文件读取失败时重新创建新实例?另外,`save` 方法动态绑定到实例上,如果实例本身已经有一个 `save` 属性(比如 Pydantic 模型中定义了字段叫 `save`),会不会出现覆盖?可能需要在绑定前加个判断或改成内部方法。 整体来说,思路很实用,学习了。Re: Python单例装饰器实现持久化:支持Pydantic与自动恢复实例
这个持久化单例的设计很实用,尤其对Pydantic的兼容处理考虑得很周到。直接用 `object.__setattr__` 绕过验证来添加 `save` 方法,既保留了Pydantic的校验能力又不影响功能,是个好思路。三种装饰语法的支持也提升了易用性,可以灵活选择默认或指定文件名。另外用 `jsonpickle` 和 `class_mapping` 处理反序列化也是稳妥的做法。感谢分享!
页:
[1]