python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python单例装饰器

Python实现支持持久化的单例装饰器

作者:huzhongqiang

本文介绍了一个支持持久化的单例装饰器single,核心特点在于兼容三种装饰语法,支持JSON文件恢复实例等内容,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下

前言

之前提到过装饰器可以实现单例模式。

这次更进一步,带来一个支持持久化的单例装饰器

一、核心代码

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 / @single() :仅单例,实例获得 save(filename) 方法
        - @single('path/file.json') / @single(filename='path/file.json') :
        指定持久化文件,首次创建时若文件存在则从中恢复;实例获得 save() 方法
        """
    if isinstance(cls_or_filename, type):
        # @single 直接装饰类
        fname = cls_or_filename.__qualname__ + '.json'  # 默认文件名是类名 + .json
        return _decorate(cls_or_filename, fname)  # type: ignore[return-value]
    else:
        fname = cls_or_filename if cls_or_filename is not None else filename
        # 注意:不能直接'return _decorate(cls, fname)',因为此时还没有传入cls
        def decorator(cls: T):
            return _decorate(cls, fname)
        return decorator

def _decorate(cls: T, fname: str) -> T:
    _instance: T = None  # 单例实例
    # 用于 jsonpickle 解码时的类映射,避免递归调用 wrapper
    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__强制添加
                object.__setattr__(_instance, 'save', types.MethodType(save, _instance))
        return _instance  # type: ignore[return-value]
    return wrapper

二、三个实现细节

支持三种装饰语法

@single                    # 无参数,默认文件名是 类名.json
@single()                 # 同上
@single('b.json')         # 指定文件名
@single(filename='b.json') # 关键字参数指定文件名

实现原理是根据第一个参数的类型判断

if isinstance(cls_or_filename, type):
    # @single 直接装饰类,此时第一个参数就是类本身
    return _decorate(cls_or_filename, fname)
else:
    # @single('file.json') 两段式调用,先返回一个 decorator,再注入 cls
    def decorator(cls: T):
        return _decorate(cls, fname)
    return decorator

关键:第二段 @single('b.json') 返回的是 decorator(cls) 而不是直接 decorate(cls, fname),因为此时 cls 还没有传入。

jsonpickle 需要 class_mapping

class_fullname = cls.__module__ + '.' + cls.__qualname__
class_mapping = {class_fullname: cls}
_instance = jsonpickle.decode(f.read(), classes=class_mapping)

如果不传 classes=class_mapping,jsonpickle 解码时会遇到问题:

class_mapping 把类的全限定名映射回原始类,确保解码正确。

object.setattr绕过 Pydantic 限制

object.__setattr__(_instance, 'save', types.MethodType(save, _instance))

问题:Pydantic 的 BaseModel 默认禁止动态添加字段,直接 _instance.save = ... 会触发验证错误。

解决:通过 object.__setattr__ 绕过 Pydantic 的 __setattr__ 拦截,直接写到实例的 __dict__ 里。同样的技巧也适用于其他禁止动态属性的库。

三、使用示例

基本用法

@single
class A:
    def __init__(self, x: int = 0):
        self.x = x

a = A(5)
print(type(a))    # <class '__main__.A'>,类型依然是 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 拦截

四、总结

要点回顾

细节说明
三种语法无参数 / @single('f.json') / @single(filename='f.json')
两段式返回有文件名时必须返回 decorator(cls),因为 cls 还没传进来
class_mappingjsonpickle 解码时需要映射类的全限定名,避免无法反序列化
object.setattr绕过 Pydantic 等禁止动态添加字段的限制

适用场景

以上就是Python实现支持持久化的单例装饰器的详细内容,更多关于Python单例装饰器的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文