单例装饰器升级:用 jsonic 过滤私有字段
标签:#Python #设计模式 #单例模式 #序列化 #编程实战 日期:2026-06-17 摘要:本文介绍单例装饰器的改进方案:改用 jsonic 替代 jsonpickle,实现序列化时过滤私有字段。同时引入
...(Ellipsis)作为默认值占位符,优雅解决"无参 vs 有参"的语义区分问题。
前言
之前写过一篇 Python实现单例装饰器:支持持久序列化,用 jsonpickle 实现了单例的持久化。但有一个缺陷:会把私有字段(如 _parent)也序列化进去。
这次升级,改用 jsonic 库,支持 serialize_private_attributes=False,轻松过滤私有成员。
一、问题回顾
之前用 jsonpickle 序列化时,所有字段都会被保存:
python
@single('task.json')
class Task:
def __init__(self):
self.id: str = 'id'
self.label: str = '新节点'
self._parent: str = '_parent' # 私有字段也被序列化了
问题是:私有字段往往是运行时状态(如缓存、临时引用),不应该持久化。
二、改进方案
1. 改用 jsonic
python
from jsonic import serialize, deserialize
jsonpickle:
python
# 序列化所有属性(包括私有)
jsonpickle.encode(self)
jsonic:
python
# 默认不包含私有属性
# 不直接使用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)作为哨兵值:
python
@single(...) # 单例 + 持久化,文件名默认用 类名.json
@single # 仅单例,不持久化
@single('x.json') # 单例 + 持久化到 x.json
实现逻辑:
python
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 需要另一种方式:
python
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 需要找到原始类,所以临时替换一下。
三、完整代码
python
from typing import TypeVar
import os
from jsonic import serialize, deserialize
import types
from functools import wraps
import sys
import json
T = 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 wrapper
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__
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 _instance
def _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 save(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, 'save'):
object.__setattr__(_instance, 'save', types.MethodType(save, _instance))
return _instance
return wrapper
def 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
四、使用示例
python
from pydantic import BaseModel
@single(...)
class Task(BaseModel):
id: str = 'id'
label: str = '新节点'
children: list[str] = []
_parent: str = '_parent' # 私有字段,不会被序列化
a = Task()
a.save() # 保存到 Task.json,不包含 _parent
运行结果(Task.json):
json
{
"id": "id",
"label": "新节点",
"children": []
}
_parent 被成功过滤了。
五、总结
| 改进点 | 说明 |
|---|---|
| jsonpickle → jsonic | jsonic 支持 serialize_private_attributes=False 过滤私有字段 |
... 作为哨兵值 |
None = 不持久化,... = 使用默认文件名 |
| 临时替换 wrapper | 反序列化时临时把 wrapper 换回原始类,避免找不到类 |
| 保持 object.setattr | 绕过 Pydantic 等禁止动态添加字段的限制 |
💡 适用场景
- 配置管理:持久化时不想保存运行时状态
- 有缓存/父子引用的对象:避免序列化时陷入死循环
- 任何需要"干净"持久化的单例
本文为本人原创,首发于掘金。 如果你有任何问题或想法,欢迎在评论区交流!