在开发中,单例模式常用于管理全局状态(如配置管理器、缓存服务)。但普通单例在程序重启后会丢失状态。本文介绍一个支持持久化的单例装饰器,可自动将实例保存到JSON文件,下次启动时恢复,且兼容Pydantic等禁止动态添加属性的库。
核心代码实现
以下是完整实现,包含三种调用方式和Pydantic兼容处理:
- import os
- import types
- from functools import wraps
- from typing import TypeVar
- import jsonpickle
- from pydantic import BaseModel
- T = TypeVar('T')
- def single(cls_or_filename: T | str = None, *, filename: str = None) -> T:
- """单例装饰器,支持持久化。
-
- 用法:
- @single # 默认文件名 类名.json
- @single()
- @single('file.json')
- @single(filename='file.json')
- """
- if isinstance(cls_or_filename, type):
- # @single 直接装饰类
- fname = cls_or_filename.__qualname__ + '.json'
- return _decorate(cls_or_filename, fname)
- else:
- fname = cls_or_filename if cls_or_filename is not None else filename
- def decorator(cls: T):
- return _decorate(cls, fname)
- return decorator
- def _decorate(cls: T, fname: str) -> T:
- _instance: T = None
- # 用于jsonpickle解码时的类映射
- class_fullname = cls.__module__ + '.' + cls.__qualname__
- class_mapping = {class_fullname: cls}
- @wraps(cls)
- def wrapper(*args, **kwargs) -> T:
- nonlocal _instance
- if _instance is None:
- if fname is not None and os.path.exists(fname):
- with open(fname, 'r', encoding='utf-8') as f:
- _instance = jsonpickle.decode(f.read(), classes=class_mapping)
- else:
- _instance = cls(*args, **kwargs)
- # 动态添加 save 方法
- if not hasattr(_instance, 'save'):
- def save(self, filename: str = None):
- save_path = filename or fname
- if save_path is None:
- raise ValueError(
- "No filename specified. Either provide a filename to save() "
- "or use the decorator with a filename."
- )
- with open(save_path, 'w', encoding='utf-8') as f:
- f.write(jsonpickle.encode(self))
- # 绕过Pydantic限制
- object.__setattr__(_instance, 'save', types.MethodType(save, _instance))
- return _instance
- return wrapper
复制代码
三个关键实现细节
一、支持三种装饰语法
- @single # 无参数,默认文件名为 类名.json
- @single() # 同上
- @single('b.json') # 指定持久化文件
- @single(filename='b.json')
实现原理:检查第一个参数类型。如果是类(type),表示直接装饰;否则返回一个decorator函数等待接收类。注意当有文件名参数时,必须返回decorator,此时cls尚未传入。
二、jsonpickle 需要 class_mapping
编码时jsonpickle会保存类的全限定名(如 __main__.MyClass)。解码时必须通过classes参数告诉它全限定名对应的类,否则无法反序列化。代码中:
- class_mapping = {class_fullname: cls}
- _instance = jsonpickle.decode(f.read(), classes=class_mapping)
复制代码
这样解码就能正确重建实例。
三、object.__setattr__ 绕过 Pydantic 限制
Pydantic的BaseModel在__setattr__中进行了验证,会拒绝动态添加的属性(如save方法)。直接赋值 _instance.save = ... 会触发ValidationError。使用object.__setattr__绕过Pydantic的验证,直接写入实例的__dict__,使之正常工作。该技巧也适用于其他禁止动态属性的库。
使用示例
基本用法(无参数)
- @single
- class A:
- def __init__(self, x: int = 0):
- self.x = x
- a = A(5)
- print(type(a)) # <class '__main__.A'>
- print(a.x) # 5
- a.save() # 保存到 A.json
复制代码
指定持久化文件
- @single('b.json')
- class B:
- def __init__(self, y: str = ''):
- self.y = y
- b = B('hello')
- b.save() # 保存到 b.json
复制代码
配合 Pydantic 使用
- @single
- class Task(BaseModel):
- id: str = 'id'
- label: str = 'label'
- children: list['Task'] = []
- t = Task()
- print(t.id) # id
- print(t.label) # label
- t.save() # 正常保存,Pydantic不会拦截
复制代码
总结与适用场景
该装饰器自动为类添加save()方法,支持三种调用语法。通过jsonpickle实现序列化,利用class_mapping保证解码正确。object.__setattr__技巧让装饰器兼容Pydantic等框架。适用于配置管理器(启动恢复,修改后持久化)、缓存服务(重启不丢失)、轻量级状态管理(无需数据库)等场景。 |