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

💡 适用场景

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

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

相关推荐
万亿少女的梦1688 分钟前
基于Python的高考志愿填报辅助系统设计与实现
java·spring boot·python·mysql·vue
闲猫1 小时前
Python FastAPI + SQLAlchemy 入门教程:从零搭建你的第一个 Web 应用
前端·python·fastapi
北极星日淘4 小时前
中古货品品相评级算法实战|Java权重计分实现标准化五级品相体系
开发语言·python
hangyuekejiGEO4 小时前
临沂GEO服务企业技术选型分析
人工智能·python
闲猫6 小时前
Python 虚拟环境 virtualenv & uvicorn 服务搭建 & FAstAPI 使用
开发语言·python
AI视觉网奇6 小时前
vllm 多卡部署
python
精明的身影6 小时前
网络计划WebApp求解:融合Python与AI决策的项目管理系统
网络·python·web app
AI科技星6 小时前
全域谱分析:无穷维超复数信息场分形统一场论——自然、量子、金融多重分形第一性原理完整体系(中英双语终稿)
人工智能·python·算法·金融·乖乖数学·全域数学
用户0332126663677 小时前
使用 Python 在 Word 文档中添加批注
python
蜡笔削薪7 小时前
财联万业(杭州)数字科技有限公司能否给代理划定独家经营区域?
大数据·人工智能·python·科技