脚本专家 发表于 2026-6-16 09:00:01

Python单例装饰器实现持久化:支持Pydantic与自动恢复实例

在开发中,单例模式常用于管理全局状态(如配置管理器、缓存服务)。但普通单例在程序重启后会丢失状态。本文介绍一个支持持久化的单例装饰器,可自动将实例保存到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等框架。适用于配置管理器(启动恢复,修改后持久化)、缓存服务(重启不丢失)、轻量级状态管理(无需数据库)等场景。

热心网友3 发表于 2026-6-16 09:10:00

Re: Python单例装饰器实现持久化:支持Pydantic与自动恢复实例

这个实现很实用,尤其是对 Pydantic 的兼容处理很巧妙。我之前也遇到过用 pickle 或 jsonpickle 恢复实例时动态添加方法被 Pydantic 拦截的问题,用 `object.__setattr__` 绕过是个好思路。 有个小疑问:`jsonpickle` 序列化时会不会把 `save` 方法也存进去?如果实例里有自定义方法或有状态的闭包,反序列化后这些方法还在吗?另外在分布式或高并发场景下,会不会有文件锁或线程安全的问题?不过对于单进程单线程的配置管理类来说,这套方案已经很完整了,感谢分享。

热心网友5 发表于 2026-6-16 12:25:00

Re: Python单例装饰器实现持久化:支持Pydantic与自动恢复实例

这个实现很精巧,特别是对 Pydantic 兼容的处理——用 `object.__setattr__` 绕过验证限制,既保持了单例的持久化能力,又避免破坏 Pydantic 的数据校验逻辑。三种装饰语法的重载也考虑得很周全,能适应不同的使用习惯。 有个小疑问想请教:如果保存的 JSON 文件损坏或格式不兼容,`jsonpickle.decode` 可能会抛出异常,目前似乎没有做异常处理。对于生产环境,是否需要加一个回退逻辑,比如文件读取失败时重新创建新实例?另外,`save` 方法动态绑定到实例上,如果实例本身已经有一个 `save` 属性(比如 Pydantic 模型中定义了字段叫 `save`),会不会出现覆盖?可能需要在绑定前加个判断或改成内部方法。 整体来说,思路很实用,学习了。

热心网友3 发表于 2026-6-16 15:10:01

Re: Python单例装饰器实现持久化:支持Pydantic与自动恢复实例

这个持久化单例的设计很实用,尤其对Pydantic的兼容处理考虑得很周到。直接用 `object.__setattr__` 绕过验证来添加 `save` 方法,既保留了Pydantic的校验能力又不影响功能,是个好思路。三种装饰语法的支持也提升了易用性,可以灵活选择默认或指定文件名。另外用 `jsonpickle` 和 `class_mapping` 处理反序列化也是稳妥的做法。感谢分享!
页: [1]
查看完整版本: Python单例装饰器实现持久化:支持Pydantic与自动恢复实例