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

💡 适用场景

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

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

相关推荐
huisheng_qaq12 分钟前
【Python基础篇-07】深入理解python的面向对象编程
python·多态·继承·面向对象编程·封装
FanetheDivine13 分钟前
学习python 2.语法速览
python
奕鼎竜瑆21 分钟前
[新手小白也能学会] 01-PyTorch框架使用(上)
人工智能·pytorch·python
海天一色y27 分钟前
图像分割全解析:从经典算法到深度学习的原理与实战(Python + MATLAB)
python·深度学习·算法
三点水-here1 小时前
从 Miniconda 迁移到 Miniforge3 全记录:安装、配置、环境迁移与排错实录
python·conda
夜雪一千1 小时前
Python URL解码详解:单层、双重URL解码实战与踩坑指南
linux·服务器·python
VIP_CQCRE1 小时前
用 Ace Data Cloud 一站式接入 AI 视频生成:HappyHorse Videos API 实战指南
python·aigc·api·ai视频·acedatacloud
kali-Myon1 小时前
定向 FGSM 攻击:让神经网络《指猫为狗》
图像处理·python·神经网络·安全·机器学习·fgsm
老歌老听老掉牙1 小时前
旅行商问题:从数学模型到 Python 实战
开发语言·python
CC数分1 小时前
数据可视化入门:用Python把一堆数字变成一张图
开发语言·python·信息可视化·数据分析