Python 3.7 中的 Pickle 中断更改
Pickle breaking change in Python 3.7
我有自定义列表和字典 类 在 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
子 classes 这可能更快。
但是,有了那个改变,测试列表对象的 unpickling 代码的方式也改变了,从
if (PyList_Check(list)) {
到
if (PyList_CheckExact(list)) {
影响您的代码的正是该更改。上面的测试寻找一个快速路径,说 如果我们有一个列表 class,然后使用 PyList_SetSlice()
加载数据,而不是显式调用 [=新实例 上的 17=] 或 .append()
方法。旧版本(Python 3.6及更早版本)接受列表和subclasses,新版本只接受list
本身,不接受subclasses!
因此,对于 Python 3.6 及更早版本,在解开您的自定义 MyList.append()
方法时不会调用 ,纯粹是因为您 subclassed list
。在 Python 3.7 中,当取消对您的自定义 MyList.extend()
方法进行 unpickling 时,会调用 方法 。这是非常有意的,子classes 应该被允许提供一个自定义的.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,一个不 return 索引 3 中的迭代器的数据结果元组的。 list().__reduce_ex__()
协议版本 2、3 和 4 returns (copyreg.__newobj__, type(self), self.__dict__, iter(self), None)
.
例如,自定义版本必须使用 (type(self), (tuple(self), self.option), None, None, None)
。这确实会带来一些额外的开销(tuple(self)
在 pickling 和 unpickling 时会占用额外的内存)。
我有自定义列表和字典 类 在 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
子 classes 这可能更快。
但是,有了那个改变,测试列表对象的 unpickling 代码的方式也改变了,从
if (PyList_Check(list)) {
到
if (PyList_CheckExact(list)) {
影响您的代码的正是该更改。上面的测试寻找一个快速路径,说 如果我们有一个列表 class,然后使用 PyList_SetSlice()
加载数据,而不是显式调用 [=新实例 上的 17=] 或 .append()
方法。旧版本(Python 3.6及更早版本)接受列表和subclasses,新版本只接受list
本身,不接受subclasses!
因此,对于 Python 3.6 及更早版本,在解开您的自定义 MyList.append()
方法时不会调用 ,纯粹是因为您 subclassed list
。在 Python 3.7 中,当取消对您的自定义 MyList.extend()
方法进行 unpickling 时,会调用 方法 。这是非常有意的,子classes 应该被允许提供一个自定义的.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,一个不 return 索引 3 中的迭代器的数据结果元组的。 list().__reduce_ex__()
协议版本 2、3 和 4 returns (copyreg.__newobj__, type(self), self.__dict__, iter(self), None)
.
例如,自定义版本必须使用 (type(self), (tuple(self), self.option), None, None, None)
。这确实会带来一些额外的开销(tuple(self)
在 pickling 和 unpickling 时会占用额外的内存)。