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

💡 适用场景

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

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

相关推荐
云梦泽࿐้1 小时前
变量与数据类型:Python世界的基石
开发语言·python
开发小能手-roy1 小时前
Lambda表达式性能陷阱:避坑指南与JIT编译优化分析
开发语言·python
风吹夏回2 小时前
RabbitMQ 核心术语 + Python pika 方法完整讲解
分布式·python·rabbitmq
爱读书的小胖2 小时前
无偿分享ChatGPT Image 2画图网页与并发绘图python程序【Ai绘图】
开发语言·python·chatgpt
cvcode_study2 小时前
Scikit-learn
python·机器学习·scikit-learn
vortex52 小时前
新手前后端开发学习指南:从Flask框架到全栈实践
后端·python·flask
你是个什么橙2 小时前
Python入门学习1:安装配置开发环境——Python或Annaconda,Pycharm
python·学习·pycharm
xxwl5852 小时前
Python语言初步认识(1)
开发语言·python·学习
古城小栈2 小时前
Python 的主流Ai框架为什么优先适配 Linux 系统?
linux·人工智能·python