单例装饰器升级:用 jsonic 过滤私有字段

单例装饰器升级:用 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 等禁止动态添加字段的限制

💡 适用场景

  • 配置管理:持久化时不想保存运行时状态
  • 有缓存/父子引用的对象:避免序列化时陷入死循环
  • 任何需要"干净"持久化的单例

本文为本人原创,首发于掘金。 如果你有任何问题或想法,欢迎在评论区交流!

相关推荐
Merrill_hh21 分钟前
Python环境搭建
python
用户0190275816126 分钟前
如何用 Python 回测布林带突破策略?
python
4SAPI36 分钟前
企业Agent落地选型:大模型API接入方案迁移实践与稳定性评估
人工智能·python·php
用户019027581611 小时前
如何用 Python 计算 CR 能量指标(中间意愿指标)?
python
曲径海盗1 小时前
【Python 学习第 34 天】条件判断 if/else
python·学习笔记·编程入门
用户6602676118791 小时前
重试不重复下单还不够:同一幂等键换了参数,应该返回什么?
python
五网1 小时前
用Python搭建创业项目财务测算模型:CAC、LTV、盈亏平衡点一键算出(附完整代码)
python
用户6602676118791 小时前
登录成功就能读取任意任务 ID 吗?给 SaaS 读写加上租户范围
python
用户7624117567051 小时前
第十五章 数据分析
python·数据分析
2601_961963382 小时前
电子合同检索与全文搜索怎么测、怎么选:六个指标 + 五家平台对比 + 八个自测问题
数据库·后端·python·django·智能合约