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

💡 适用场景

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

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

相关推荐
小刘在重生~26 分钟前
Java 常用类|包装类 装箱拆箱、Integer 缓存面试题
java·python·缓存
2601_962099461 小时前
Python xlwt设置excel单元格字体及格式
python·excel·xlwt·样式设置·单元格格式
奇牙coding1231 小时前
GPT-6-Astra API 接入教程:OpenRouter 路由配置 + Python/curl 示例 + 静默降级踩坑
开发语言·python·gpt·ai
2601_962298272 小时前
Python与Selenium结合的Web自动化测试全流程实践教程
自动化测试·python·selenium·web测试·pageobjectmodel
天衍四九-2 小时前
第一章:从 LLM 到 Agent —— DeepSeek Harness 入门
网络·数据库·人工智能·python
529宝宝起名网2 小时前
用 Python 实现名字寓意评分算法:基于 NLP 语义分析的名字内涵深度评估
python·算法·自然语言处理
2601_962298672 小时前
Python:对象缓存优化机制(CPython)
python·性能优化·内存优化·cpython·对象缓存
枫彩3 小时前
A股数据源怎么选?用 Python 验收 AI 复盘的日期与明细
python·ai agent·股票数据·mcp
2601_962382433 小时前
Python 代码不可不知的函数式编程技术
python·函数式编程·高阶函数·嵌套函数·头等函数
2601_962078033 小时前
Python办公自动化与数据分析实战 培训班2021年
python·数据分析·办公自动化·自动化办公·实战演练