Python实现单例装饰器:支持持久序列化
标签:#Python #设计模式 #单例模式 #序列化 #编程实战 日期:2026-06-15 摘要:本文介绍一个支持持久化的单例装饰器
single,核心特点在于:兼容三种装饰语法、支持 JSON 文件恢复实例、巧用object.__setattr__绕过 Pydantic 字段限制、用 jsonpickle 的class_mapping避免无限解码循环。
前言
之前写过一篇 # Python 单例模式的几种实现方式:朴素才是王道,提到过装饰器可以实现单例模式。
这次更进一步,带来一个支持持久化的单例装饰器:
- 免参数装饰
@single:实例自动获得save()方法 - 带文件名装饰
@single('file.json'):启动时从文件恢复,调用save()默认保存到该文件 - Pydantic 兼容:即使
BaseModel禁止动态添加字段也能工作
一、核心代码
python
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
二、三个实现细节
1. 支持三种装饰语法
python
@single # 无参数,默认文件名是 类名.json
@single() # 同上
@single('b.json') # 指定文件名
@single(filename='b.json') # 关键字参数指定文件名
实现原理是根据第一个参数的类型判断:
python
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 还没有传入。
2. jsonpickle 需要 class_mapping
python
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 把类的全限定名映射回原始类,确保解码正确。
3. object.setattr 绕过 Pydantic 限制
python
object.__setattr__(_instance, 'save', types.MethodType(save, _instance))
问题 :Pydantic 的 BaseModel 默认禁止动态添加字段,直接 _instance.save = ... 会触发验证错误。
解决 :通过 object.__setattr__ 绕过 Pydantic 的 __setattr__ 拦截,直接写到实例的 __dict__ 里。同样的技巧也适用于其他禁止动态属性的库。
三、使用示例
基本用法
python
@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
指定持久化文件
python
@single('b.json')
class B:
def __init__(self, y: str = ''):
self.y = y
b = B('hello')
b.save() # 保存到 b.json
配合 Pydantic 使用
python
@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_mapping | jsonpickle 解码时需要映射类的全限定名,避免无法反序列化 |
| object.setattr | 绕过 Pydantic 等禁止动态添加字段的限制 |
💡 适用场景
- 配置管理器:启动时恢复配置,修改后自动持久化
- 缓存服务:重启后缓存不丢失
- 轻量级状态管理:不需要数据库
本文为本人原创,首发于掘金。 如果你有任何问题或想法,欢迎在评论区交流!