腌制具有 __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)
如何使用 __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 useobject.__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 withunsafe_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)