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 在那个时候仍然是未定义的。

我必须直接从 dictlist 继承,并且我确实需要在我的代码中覆盖 appendextend 函数。有没有办法预先设置 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 时会占用额外的内存)。