当前位置:

首页 > 编程开发 > 单例装饰器升级:用 jsonic 过滤私有字段

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

单例装饰器改用Jsonic序列化,在序列化时通过参数过滤私有字段;引入省略号作为哨兵值表示使用类名作默认文件名;反序列化时临时将包装类替换为原始类,可避免因装饰器干扰导致类查找失败。

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


在上一篇文章中,我们实现了一个支持持久化序列化的单例装饰器,用的是 jsonpickle。但有个小问题:私有字段(比如 _parent)也会被一股脑儿序列化进去,这显然不是我们想要的结果——私有字段通常只是运行时状态,比如缓存或临时引用,用不着保存到文件里。

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

这次升级,我们把序列化工具换成了 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 → jsonicjsonic 支持 serialize_private_attributes=False 过滤私有字段
... 作为哨兵值None = 不持久化,... = 使用默认文件名
临时替换 wrapper反序列化时临时把 wrapper 换回原始类,避免找不到类
保持 object.__setattr__绕过 Pydantic 等禁止动态添加字段的限制

适用场景

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

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
C++动态数组初始化怎么写?常用语句与代码示例
C++动态数组初始化怎么写?常用语句与代码示例

深入解析C++中动态数组的初始化机制,涵盖new操作符的不同用法、基本类型与类对象的初始化差异,以及为何在现代C++开发中应优先使用std::vector。

好衣库售后怎么样?把保障体系拆开看
好衣库售后怎么样?把保障体系拆开看

好衣库售后全程由平台统一受理,绝不甩给店主,店主零售后负担。物流合作顺丰、京东、菜鸟三大优质快递,退货运费险由人保、众安保承保。消费者全面享直购标准,退换货由平台全额兜底,买卖双方均安心省心无忧。

全电作业新标杆!2026款长安猎手K50上市,14.19万起重塑皮卡体验
全电作业新标杆!2026款长安猎手K50上市,14.19万起重塑皮卡体验

2026款长安猎手K50增程皮卡上市,售价14.19万至17.89万元。搭载2.0T超级大增程系统,双电机峰值功率240kW,百公里加速5.9秒。配备宁德时代2C电池,纯电续航192公里,总电力储备239kWh,支持30kW+3.3kW双功率供电,实现全场景用电覆盖。

2026年车市新势力崛起:新车型成销量担当,零跑A10单月破2万领跑
2026年车市新势力崛起:新车型成销量担当,零跑A10单月破2万领跑

2026年5月,国内乘用车出口78.4万辆,同比增长75.1%,占厂商总销量35%。新车型成为增长核心引擎,零跑A10以2.2万辆登顶新车型冠军。消费者需求加速从功能满足转向体验升级,汽车产业进入高速迭代周期,推动行业洗牌。

十七载深耕智能领域,长安“天枢领航”亮相车展引领安全出行新体验
十七载深耕智能领域,长安“天枢领航”亮相车展引领安全出行新体验

长安汽车在重庆车展发布“天枢领航”辅助驾驶系统,从安全防护、减轻压力到智能交互三个维度升级出行体验。该系统基于十七年技术深耕,依托天枢智能实验中心积累超40万虚拟场景,实测500万公里覆盖185类典型场景,形成技术-场景-用户闭环优化机制。

《2025年度北京历史文化名城保护大事记》发布
《2025年度北京历史文化名城保护大事记》发布

2025年度北京历史文化名城保护大事记发布,收录94项代表性事件。关键进展包括《北京中轴线世界文化遗产保护条例》施行、先农坛校址腾退实现空间贯通、长辛店老镇启动区开街、路县故城遗址公园试运行等,标志保护工作从被动抢救转向主动规划。

DBeaver 25.3.3 发布
DBeaver 25.3.3 发布

DBeaver25.3.3发布,优化SQL编辑器响应速度与资源占用,AI辅助自动禁用不兼容模型的temperature参数。数据编辑器默认对SELECT查询自动统计行数,修复数据迁移对话框尺寸异常。重构自动提交逻辑,升级UI布局,修正事务状态显示及macOS欢迎画面。数据库适配增强:ClickHouse修复空格解析,Sybase修复双击表打开,DuckDB新

REDMI 17与Note 17系列通过认证 Note 17系列或7、8月发布
REDMI 17与Note 17系列通过认证 Note 17系列或7、8月发布

红米十七与Note十七系列已经通过多项国际认证。红米十七搭载骁龙六sGen二四G版,含日本专属版本和POCO联名版C九五Pro。Note十七系列由三款机型组成,均基于骁龙平台,预计二零二六年七至八月率先在中国上市。

美股科技板块剧烈震荡:心片股集体承压 ARM跌超6%
美股科技板块剧烈震荡:心片股集体承压 ARM跌超6%

当地时间6月9日,美股三大指数走势分化,纳指与标普500双双收跌。科技板块调整明显,半导体ETF盘中最大跌幅超7%,费城半导体指数一度大跌逾8.6%。ARM跌超6%,Coherent跌逾11%,部分AI相关个股也显著回调。

macOS Golden Gate调整菜单设计 移除菜单项图标
macOS Golden Gate调整菜单设计 移除菜单项图标

macOSGoldenGate更新后移除了菜单项图标,回归简洁纯文字布局。此前Tahoe版本引入图标但辨识度低,反而拖慢浏览效率。此次调整遵循简洁原则,显著提升可读性与操作效率,是对用户反馈的理性精简。这一改动减少视觉干扰,菜单栏更清晰,用户可快速定位功能。

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。