此代码(使用自定义
list
子类)对我来说效果很好:
import pickle
class Numbers(list):
def __init__(self, *numbers: int) -> None:
super().__init__()
self.extend(numbers)
numbers = Numbers(12, 34, 56)
numbers.append(78)
numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
但是当我将父类更改为
set
时:
import pickle
class Numbers(set):
def __init__(self, *numbers: int) -> None:
super().__init__()
self.update(numbers)
numbers = Numbers(12, 34, 56)
numbers.add(78)
numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
代码会通过以下回溯引发
TypeError
:
Traceback (most recent call last):
File "test.py", line 11, in <module>
numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
File "test.py", line 6, in __init__
self.update(numbers)
TypeError: unhashable type: 'list'
set
子类已成功初始化并且运行良好,但尝试腌制它会引发一个非常令人困惑的异常,因为实际上我的代码中没有使用list
。
修改内置类型的构造函数非常困难且容易出错,因为其他方法可能依赖于它。尽可能避免。
首先,通过强制使用 Python 实现 覆盖 pickle
模块的
已编译 C 实现,我们可以在回溯中获得更多信息:
Traceback (most recent call last):
File "test.py", line 14, in <module>
numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
File "/usr/local/lib/python3.7/pickle.py", line 1604, in _loads
encoding=encoding, errors=errors).load()
File "/usr/local/lib/python3.7/pickle.py", line 1086, in load
dispatch[key[0]](self)
File "/usr/local/lib/python3.7/pickle.py", line 1437, in load_reduce
stack[-1] = func(*args)
File "test.py", line 6, in __init__
self.update(numbers)
TypeError: unhashable type: 'list'
尽管文档指出:
当一个类实例被 unpickle 时,它的
方法通常不会被调用。__init__
我们可以从上面的回溯中看到,如果类的 pickled 表示包含
__init__
操作码(通常当类实现自定义 REDUCE
方法时),则调用
__reduce__
方法,并且,作为检查腌制表示显示,
REDUCE
操作码确实存在:
0: \x80 PROTO 3
2: c GLOBAL '__main__ Numbers'
20: q BINPUT 0
22: ] EMPTY_LIST
23: q BINPUT 1
25: ( MARK
26: K BININT1 56
28: K BININT1 34
30: K BININT1 12
32: K BININT1 78
34: e APPENDS (MARK at 25)
35: \x85 TUPLE1
36: q BINPUT 2
38: R REDUCE
39: q BINPUT 3
41: } EMPTY_DICT
42: q BINPUT 4
44: b BUILD
45: . STOP
解决方案
import pickle
class Numbers(set):
pass
numbers = Numbers([12, 34, 56])
numbers.add(78)
numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
或者,如果你真的必须这样做,至少确保将所有参数传递给父构造函数:
import pickle
class Numbers(set):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
numbers = Numbers([12, 34, 56])
numbers.add(78)
numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
pickle.dumps()
实际上是调用对象的
__reduce_ex_()
方法来序列化。
__reduce_ex__()
是
__reduce__()
方法的更新版本,如果未定义,它将调用
__reduce__()
来代替。对于
set
对象,
__reduce_ex__(<protocol>)
相当于
__reduce__()
,它返回一个包含三个项目的元组:
(<the class of instance>, (a tuple storing arguments), {a dict storing attributes})
您的情况:
class Numbers(set):
def __init__(self, *numbers: int) -> None:
super().__init__()
self.update(numbers)
Numbers
继承自
set
,并且您不重写
__reduce__
和
__reduce_ex__
方法,因此它遵循
set
类的模式。尝试一下你可能会看到:
numbers = Numbers(12, 34, 56)
pickled = numbers.__reduce__()
# print(pickled)
# (<class '__main__.Numbers'>, ([56, 34, 12],), None)
pickle.loads()
则相反,它使用元组来重建实例。它首先使用元组中的第一项和第二项创建一个实例,然后使用第三个元素更新属性同样,
instance = pickled[0](*pickled[1])
instance.__dict__.update(pickled[2])
如果您运行此类代码,它会准确地重现您发布的错误。再看一下:
pickled[0]
是对类
Numbers
,的引用
pickled[1]
是元组
([56, 34, 12],)
,它首先被
*
运算符解包为列表
[56, 34, 12]
,然后这个列表作为类
Numbers
,的参数传递
pickle.loads()
的第一步其实就是做
Numbers([56, 34, 12])
def __init__(self, *numbers: int) -> None:
,因此列表
[56, 34, 12]
是传递给
__init__
方法的唯一单个参数,然后将其传递给
self.update([56, 34, 12])
,
self
确实是对
set
类对象的引用,一个
set
不能接受
list
对象作为元素,因为它是可变的(即不可哈希)。这就是你的示例中失败的原因。
为了使您的示例正常工作,可以直接覆盖
__reduce__()
方法。
class Numbers(set):
def __init__(self, *numbers: int) -> None:
super().__init__()
self.update(numbers)
def __reduce__(self):
return (self.__class__, tuple(num for num in self), self.__dict__)
然后再试一次:
import pickle
numbers = Numbers(12, 34, 56)
print(numbers)
# Numbers({56, 34, 12})
numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
print(numbers)
# Numbers({56, 34, 12})
工作得很好,没有出现异常!