单例装饰器升级:用 jsonic 过滤私有字段
单例装饰器改用Jsonic序列化,在序列化时通过参数过滤私有字段;引入省略号作为哨兵值表示使用类名作默认文件名;反序列化时临时将包装类替换为原始类,可避免因装饰器干扰导致类查找失败。
单例装饰器升级:用 jsonic 过滤私有字段
在上一篇文章中,我们实现了一个支持持久化序列化的单例装饰器,用的是 jsonpickle。但有个小问题:私有字段(比如 _parent)也会被一股脑儿序列化进去,这显然不是我们想要的结果——私有字段通常只是运行时状态,比如缓存或临时引用,用不着保存到文件里。

这次升级,我们把序列化工具换成了 jsonic,它支持一个参数 serialize_private_attributes=False,能轻松过滤掉私有成员,干净利落。
一、问题回顾
之前用 jsonpickle 序列化时,所有字段都会被保存下来:
@single('task.json')
class Task:
def __init__(self):
self.id: str = 'id'
self.label: str = '新节点'
self._parent: str = '_parent' # 私有字段也被序列化了
问题是,私有字段往往只服务于运行时逻辑(比如缓存、临时引用),完全没有必要持久化到磁盘。
二、改进方案
1. 改用 jsonic
from jsonic import serialize, deserialize
jsonpickle:
# 序列化所有属性(包括私有)
jsonpickle.encode(self)
jsonic:
# 默认不包含私有属性
# 不直接使用string_output=True, 因为它不能正确输出中文
data = serialize(self, string_output=False, serialize_private_attributes=False)
f.write(json.dumps(data, ensure_ascii=False, indent=2))
serialize_private_attributes=False 会自动过滤所有以 _ 开头的私有字段,这正是我们需要的。
2. 用 ... 代表"无参数"
之前 @single 和 @single('file.json') 的语义区分有点模糊:
@single:仅单例,不持久化@single('file.json'):单例 + 持久化到指定文件
但问题来了:如果我想仅单例 + 用类名作为文件名(也就是默认持久化),该怎么写?
解决方案:用 ...(Ellipsis)作为哨兵值:
@single(...) # 单例 + 持久化,文件名默认用 类名.json
@single # 仅单例,不持久化
@single('x.json') # 单例 + 持久化到 x.json
实现逻辑也很清晰:
def single(cls_or_filename: T | str = None, *, filename: str = None) -> T:
# 仅单例,不持久化
if isinstance(cls_or_filename, type):
return _decorate_cls(cls_or_filename)
# 获取文件名
fname = cls_or_filename if cls_or_filename is not None else filename
def decorator(cls: T):
# 如果传入的是 ...,用类名作为文件名
if fname is ...:
fname = cls.__qualname__ + '.json'
return _decorate(cls, fname)
return decorator
为什么选 ...? 原因很自然:
None用来表示"不指定文件名"...用来表示"使用默认值(类名作为文件名)"- 语义上
...就是"这里本应有值,但现在先用默认的"
3. 反序列化时避免找不到类
jsonpickle 可以用 classes=class_mapping 来指定类,jsonic 需要换一种方式:
def _load_from_file(cls: T, fname: str) -> T:
with open(fname, 'r', encoding='utf-8') as f:
raw_str = f.read()
module = sys.modules[cls.__module__]
class_name = cls.__name__
# 临时将模块中的 wrapper 替换为原始类,以免反序列化时找不到原始类
original_value = getattr(module, class_name, None)
setattr(module, class_name, cls)
try:
_instance = deserialize(raw_str, string_input=True)
finally:
# 恢复原来的值(即 wrapper 函数)
setattr(module, class_name, original_value)
return _instance
原理:装饰后,模块里的 Task 其实已经被 wrapper 覆盖了,不再是原始类。反序列化时 jsonic 需要找到原始类,所以临时把 wrapper 换回原始类,反序列化完成后再恢复。这招虽然看起来有点“暴力”,但效果很稳。
三、完整代码
from typing import TypeVar
import os
from jsonic import serialize, deserialize
import types
from functools import wraps
import sys
import jsonT = TypeVar('T')def _decorate_cls(cls: T) -> T:
'''仅单例模式,不持久化'''
_instance: T = None
@wraps(cls)
def wrapper(*args, **kwargs) -> T:
nonlocal _instance
if _instance is None:
_instance = cls(*args, **kwargs)
return _instance
return wrapperdef _load_from_file(cls: T, fname: str) -> T:
'''从文件中加载实例'''
with open(fname, 'r', encoding='utf-8') as f:
raw_str = f.read()
module = sys.modules[cls.__module__]
class_name = cls.__name__
original_value = getattr(module, class_name, None)
setattr(module, class_name, cls)
try:
_instance = deserialize(raw_str, string_input=True)
finally:
setattr(module, class_name, original_value)
return _instancedef _decorate(cls: T, fname: str) -> T:
'''单例模式 + 持久化'''
_instance: T = None
@wraps(cls)
def wrapper(*args, **kwargs) -> T:
nonlocal _instance
if _instance is None:
if not os.path.exists(fname):
_instance = cls(*args, **kwargs)
else:
_instance = _load_from_file(cls, fname)
def sa ve(self, filename: str = fname):
with open(filename, 'w', encoding='utf-8') as f:
# serialize_private_attributes=False 过滤掉私有字段
data = serialize(self, string_output=False, serialize_private_attributes=False)
f.write(json.dumps(data, ensure_ascii=False, indent=2))
if not hasattr(_instance, 'sa ve'):
object.__setattr__(_instance, 'sa ve', types.MethodType(sa ve, _instance))
return _instance
return wrapperdef single(cls_or_filename: T | str = None, *, filename: str = None) -> T:
'''单例装饰器:
- @single:仅单例模式
- @single('path/file.json'):单例 + 持久化到指定文件
- @single(...):单例 + 持久化,文件名默认用类名.json
'''
if isinstance(cls_or_filename, type):
return _decorate_cls(cls_or_filename)
fname = cls_or_filename if cls_or_filename is not None else filename
def decorator(cls: T):
if fname is ...:
fname = cls.__qualname__ + '.json'
return _decorate(cls, fname)
return decorator
四、使用示例
from pydantic import BaseModel@single(...)
class Task(BaseModel):
id: str = 'id'
label: str = '新节点'
children: list[str] = []
_parent: str = '_parent' # 私有字段,不会被序列化a = Task()
a.sa ve() # 保存到 Task.json,不包含 _parent
运行后生成的 Task.json 内容如下:
{
"id": "id",
"label": "新节点",
"children": []
}
_parent 被成功过滤了,干干净净。
五、总结
| 改进点 | 说明 |
|---|---|
| jsonpickle → jsonic | jsonic 支持 serialize_private_attributes=False 过滤私有字段 |
... 作为哨兵值 | None = 不持久化,... = 使用默认文件名 |
| 临时替换 wrapper | 反序列化时临时把 wrapper 换回原始类,避免找不到类 |
| 保持 object.__setattr__ | 绕过 Pydantic 等禁止动态添加字段的限制 |
适用场景
- 配置管理:持久化时不想保存运行时状态
- 有缓存/父子引用的对象:避免序列化时陷入死循环
- 任何需要"干净"持久化的单例
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















