13

如何腌制冻结数据类的实例__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__。这只是一个错误吗?

4

3 回答 3

12

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

默认值在第 6220 行__setstate__中定义。load_build_pickle.c

对于状态字典中的项目,__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__.

文档对此给出了某种警告:

使用frozen=True 时会有微小的性能损失:__init__()不能使用简单的赋值来初始化字段,必须使用object.__setattr__().

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

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)

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

于 2019-03-22T20:09:03.743 回答
4

从 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)
于 2021-10-13T13:41:49.040 回答
0

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

相关的python文档指出:

虽然不推荐,但您可以强制dataclass()使用__hash__(). unsafe_hash=True如果您的类在逻辑上是不可变的,但仍然可以发生变异,则可能会出现这种情况。这是一个专门的用例,应该仔细考虑。

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!
于 2021-10-13T06:37:57.613 回答