我的自定义列表和字典类在 Python 3.7 中解封时不再起作用。
import pickle
class A(dict):
pass
class MyList(list):
def __init__(self, iterable=None, option=A):
self.option=option
if iterable:
for x in iterable:
self.append(x)
def append(self, obj):
if isinstance(obj, dict):
obj = self.option(obj)
super(MyList, self).append(obj)
def extend(self, iterable):
for item in iterable:
self.append(item)
if __name__ == '__main__':
pickle_file = 'test_pickle'
my_list = MyList([{'a': 1}])
pickle.dump(my_list, open(pickle_file, 'wb'))
loaded = pickle.load(open(pickle_file, 'rb'))
print(isinstance(loaded[0], A))
在 Python 2.6 到 3.6 上工作正常:
"C:\Program Files\Python36\python.exe" issue.py
True
但在 3.7 中不再正确设置 self.option
。
"C:\Program Files\Python37\python.exe" issue.py
Traceback (most recent call last):
File "issue.py", line 28, in <module>
loaded = pickle.load(open(pickle_file, 'rb'))
File "issue.py", line 21, in extend
self.append(item)
File "issue.py", line 16, in append
obj = self.option(obj)
AttributeError: 'MyList' object has no attribute 'option'
如果我要删除 extend
函数,它会按预期工作。
我也尝试过添加 __setstate__
,但在 extend
之前没有调用它,所以 option
在那一点上仍然未定义。
我必须直接从dict
和list
继承,而且我确实需要覆盖append
和extend
我的代码中的函数。有没有办法预先设置 option
或其他修复方法?是否记录了这种行为变化及其合理性?
谢谢你的时间
最佳答案
Unpickling 列表对象 switched from using list.append()
to list.extend()
,因为对于某些 list
子类来说,这可能会更快。
但是,有了这个改变,测试列表对象的 unpickling 代码的方式也改变了,从
if (PyList_Check(list)) {
到
if (PyList_CheckExact(list)) {
正是这种改变影响了您的代码。上面的测试寻找一个快速路径,说 如果我们有一个列表类,然后使用 PyList_SetSlice()
加载数据,而不是显式调用 的较慢路径.extend()
或 .append()
新实例上的方法。旧版本(Python 3.6 及更早版本)接受列表和子类,新版本只接受list
本身,不接受子类!
因此,对于 Python 3.6 及更早版本,当取消选中您的自定义 MyList.append()
方法时,不会调用,这纯粹是因为您子类化了 list
.在 Python 3.7 中,当您的自定义 MyList.extend()
方法被 unpickling 时,被 调用。这是非常有意的,子类应该被允许提供自定义的 .extend()
方法,该方法在 unpickling 时被调用。
解决方法很简单。您的数据在解封时已经包装,您不需要重新应用该包装。当您没有设置 self.option
时,只需跳过应用它:
def append(self, obj):
if isinstance(obj, dict):
try:
obj = self.option(obj)
except AttributeError:
# something's wrong, are we unpickling on Python 3.7 or newer?
if 'option' in self.__dict__:
# no, we are not, because 'option' has been set, this must
# be an error in the option() call, so re-raise
raise
# yes, we are, just ignore this, obj is already wrapped
super(MyList, self).append(obj)
这一切确实意味着您不能依赖任何已恢复的实例属性。如果这是一个大问题(你仍然需要在 unpickling 时查询实例状态),那么你将不得不提供一个不同的 __reduce_ex__
method ,它不会将数据作为结果元组索引 3 中的迭代器返回。 list().__reduce_ex__()
对于协议(protocol)版本 2、3 和 4 返回 (copyreg.__newobj__, type(self), self.__dict__, iter(self), None)
.
例如,自定义版本必须使用 (type(self), (tuple(self), self.option), None, None, None)
。这确实会带来一些额外的开销(tuple(self)
在 pickling 和 unpickling 时会占用额外的内存)。
关于python - Python 3.7 中的 Pickle 重大变化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52333864/