腌制具有 __slots__ 的冻结数据类

Pickle a frozen dataclass that has __slots__

如何使用 __slots__ 腌制冻结数据类的实例?例如,以下代码在 Python 3.7.0 中引发异常:

import pickle
from dataclasses import dataclass

@dataclass(frozen=True)
class A:
  __slots__ = ('a',)
  a: int

b = pickle.dumps(A(5))
pickle.loads(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'a'

如果我删除 frozen__slots__,这将起作用。这只是一个错误吗?

问题来自 pickle 在设置插槽状态时使用实例的 __setattr__ 方法。

默认__setstate__定义在_pickle.c line 6220中的load_build

对于state dict中的项,直接更新实例__dict__

 if (PyObject_SetItem(dict, d_key, d_value) < 0)

而对于 slotstate 字典中的项目,使用实例的 __setattr__

if (PyObject_SetAttr(inst, d_key, d_value) < 0)

现在因为实例被冻结,__setattr__ 在加载时引发 FrozenInstanceError

为了避免这种情况,您可以定义自己的 __setstate__ 方法,它将使用 object.__setattr__,而不是实例的 __setattr__

docs 对此给出了某种警告:

There is a tiny performance penalty when using frozen=True: __init__() cannot use simple assignment to initialize fields, and must use object.__setattr__().

定义 __getstate__ 也可能很好,因为实例 __dict__ 在您的情况下始终是 None。如果不这样做,__setstate__state 参数将是一个元组 (None, {'a': 5}),第一个值是实例 __dict__ 的值,第二个是 slotstate dict。

import pickle
from dataclasses import dataclass

@dataclass(frozen=True)
class A:
    __slots__ = ('a',)
    a: int

    def __getstate__(self):
        return dict(
            (slot, getattr(self, slot))
            for slot in self.__slots__
            if hasattr(self, slot)
        )

    def __setstate__(self, state):
        for slot, value in state.items():
            object.__setattr__(self, slot, value) # <- use object.__setattr__


b = pickle.dumps(A(5))
pickle.loads(b)

我个人不会将其称为错误,因为 pickling 过程被设计为灵活的,但仍有改进功能的空间。酸洗协议的修订可以在未来解决这个问题。除非我遗漏了一些东西并且除了 微小的性能损失 之外,对所有插槽使用 PyObject_GenericSetattr 可能是一个合理的解决方案?

如果您只需要 class 可哈希,您可以使用 unsafe_hash=True 选项强制生成 __hash__ 函数。您不会获得不变性保证,但 python 中的不变性无论如何是不可能的。

Relevant python documentation 状态:

Although not recommended, you can force dataclass() to create a __hash__() method with unsafe_hash=True. This might be the case if your class is logically immutable but can nonetheless be mutated. This is a specialized use case and should be considered carefully.

import pickle
from dataclasses import dataclass

@dataclass(unsafe_hash=True)
class A:
    __slots__ = ('a',)
    a: int

b = pickle.dumps(A(5))
hash(pickle.loads(b))  # works and can hash!

从 Python 3.10.0 开始,这有效,但前提是您通过数据类装饰器中的 slots=True 指定插槽。它不起作用,并且可能永远不会起作用,手动指定 __slots__

import pickle
from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class A:
  a: int

b = pickle.dumps(A(5))
pickle.loads(b)  # A(a=5)