You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为attrs.frozen冻结类的__new__添加缓存实现,咨询潜在风险

冻结类的实例缓存实现:潜在问题与优化建议

问题背景

我有一个被attrs.frozen装饰的冻结类,程序需要创建该类的大量实例。由于这些实例是不可变的且仅用于读取操作,我希望添加缓存机制,使得使用相同参数创建的对象直接从缓存获取而非重新创建。已完成实现,但不确定方案是否存在潜在问题。

现有实现代码

from typing import ClassVar

from attrs import frozen

@frozen
class Cached:
    _cache: ClassVar[dict[str, "Cached"]] = {}

    name: str
    config: int

    def __new__(cls, *args, **kwds):
        key = str(args) + str(kwds)

        if (instance:=cls._cache.get(key)) is not None:
            print("cache misses")
            return instance

        print("cache hits")
        instance = super().__new__(cls)
        instance.__init__(*args, **kwds)
        cls._cache[key] = instance
        return instance

运行结果(IPython)

In [1]: from cached_new import Cached

In [2]: c0 = Cached("cache", 0)
cache hits

In [3]: c1 = Cached("cache", 0)
cache misses

In [4]: c0 is c1
Out[4]: True

In [5]: c2 = Cached("", 1)
cache hits

In [6]: c3 = Cached("", 1)
cache misses

In [7]: c2 is c3
Out[7]: True

潜在问题分析

1. 缓存键冲突风险

当前用str(args) + str(kwds)生成缓存键,存在明显缺陷:

  • 不同参数组合可能生成相同字符串,比如Cached(1, 2)和Cached("(1, 2)", "")会得到完全一致的键,导致缓存错误匹配。
  • 位置参数与等价关键字参数无法被识别,比如Cached("cache", 0)和Cached(name="cache", config=0)会生成不同键,导致重复创建实例,缓存失效。

2. 内存泄漏风险

缓存使用ClassVar的普通字典存储实例引用,只要类存在,所有缓存实例都不会被垃圾回收。若实例数量庞大或持有大对象/外部资源,会导致内存占用持续增长,无法释放。

3. 线程安全问题

实现未添加同步机制,多线程环境下同时创建相同参数实例时,会出现竞态条件:可能同时生成多个重复实例,或缓存写入时出现数据不一致。

4. 子类继承问题

子类会共享父类的缓存字典,引发两个问题:

  • 子类实例可能被误匹配到父类的缓存实例,引发类型错误。
  • 不同子类的实例可能因键冲突被错误复用。

5. attrs兼容性隐患

手动调用instance.__init__(*args, **kwds)可能与attrs.frozen的内部初始化逻辑冲突。虽当前运行正常,但在attrs新版本或复杂场景(如带转换器、验证器的字段)下,可能触发未知问题。

优化建议

1. 安全生成缓存键

改用可哈希的参数组合作为键,避免字符串冲突:

# 将位置参数和排序后的关键字参数组合成元组,确保等价参数生成相同键
key = (args, tuple(sorted(kwds.items())))

2. 避免内存泄漏

使用weakref.WeakValueDictionary替代普通字典,实例无外部引用时会被自动回收:

from typing import ClassVar
import weakref

@frozen
class Cached:
    _cache: ClassVar[weakref.WeakValueDictionary] = weakref.WeakValueDictionary()
    # 其余逻辑适配该缓存结构

3. 保证线程安全

添加线程锁,确保缓存读写操作原子性:

from typing import ClassVar
import threading

@frozen
class Cached:
    _cache: ClassVar[dict] = {}
    _lock: ClassVar[threading.Lock] = threading.Lock()

    def __new__(cls, *args, **kwds):
        key = (args, tuple(sorted(kwds.items())))
        with cls._lock:
            if instance := cls._cache.get(key):
                print("cache misses")
                return instance
            # 实例创建与缓存写入逻辑

4. 支持子类继承

通过元类为每个子类创建独立缓存:

from typing import ClassVar
import weakref

class CachedMeta(type):
    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)
        cls._cache: ClassVar[weakref.WeakValueDictionary] = weakref.WeakValueDictionary()

@frozen
class Cached(metaclass=CachedMeta):
    name: str
    config: int

    def __new__(cls, *args, **kwds):
        key = (args, tuple(sorted(kwds.items())))
        if instance := cls._cache.get(key):
            print("cache misses")
            return instance
        print("cache hits")
        instance = super().__new__(cls)
        instance.__init__(*args, **kwds)
        cls._cache[key] = instance
        return instance

5. 更优雅的attrs兼容实现

利用functools.lru_cache简化代码,同时兼容attrs机制:

from typing import ClassVar
from functools import lru_cache
from attrs import frozen

@frozen
class Cached:
    name: str
    config: int

    def __new__(cls, *args, **kwargs):
        # 统一参数格式,确保等价参数生成相同键
        normalized_args = args
        normalized_kwargs = tuple(sorted(kwargs.items()))
        return cls._create_instance(*normalized_args, *normalized_kwargs)

    @classmethod
    @lru_cache(maxsize=None)
    def _create_instance(cls, *args):
        # 拆分位置参数和关键字参数
        pos_args = args[:len(cls.__annotations__)]
        kw_args = dict(args[len(cls.__annotations__):])
        return cls(*pos_args, **kw_args)

内容的提问来源于stack exchange,提问作者hamnghi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 20:10:31