Pickle打破了Python 3.7的变化

Cas*_*mon 6 python pickle python-3.x

我有自定义列表和字典类,在Python 3.7中进行unpickling时不再有效.

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))
Run Code Online (Sandbox Code Playgroud)

适用于Python 2.6到3.6:

"C:\Program Files\Python36\python.exe" issue.py
True
Run Code Online (Sandbox Code Playgroud)

但是不再self.option在3.7中正确设置.

"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'
Run Code Online (Sandbox Code Playgroud)

如果我要删除该extend功能,它会按预期工作.

我也试过添加__setstate__,但之前没有调用过,extend所以option在那一点上仍未定义.

我必须直接从dict和继承list,并且我需要覆盖我的代码中的appendextend函数.有没有办法option预先设置或另一个修复?行为的这种变化是否记录在案并且是否合理?

感谢您的时间

Mar*_*ers 5

取消列表对象从使用切换list.update()list.extend(),因为对于某些list子类来说这可能更快.

但是,通过这种更改,对列表对象测试的unpickling代码的方式也发生了变化

if (PyList_Check(list)) {
Run Code Online (Sandbox Code Playgroud)

if (PyList_CheckExact(list)) {
Run Code Online (Sandbox Code Playgroud)

这种变化会影响您的代码.上面的测试寻找一个快速路径,说如果我们有一个列表类,然后用于PyList_SetSlice()加载数据,而不是在新实例上显式调用.extend()or .append()方法的较慢路径.旧版本(Python 3.6及更早版本)接受列表和子类,新版本只接受list自身,而不是子类!

因此,对于Python 3.6及更早版本,MyList.append()调用自定义方法时不会调用.在Python 3.7,取储存您的自定义,当list方法调用.这是非常有意的,允许子类提供MyList.extend()在unpickling时调用的自定义方法.

解决方法很简单.在取消渲染时,您的数据已经被包装,您不需要重新应用该包装器.如果您没有.extend()设置,只需跳过应用它:

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)
Run Code Online (Sandbox Code Playgroud)

这一切都意味着您不能依赖已经恢复的任何实例属性.如果这是一个大问题(你仍然需要在unpickling时参考实例状态),那么你将不得不提供一个不同的self.option方法,一个不返回数据作为结果元组的索引3中的迭代器.__reduce_ex__对于协议版本2,3和4返回list().__reduce_ex__().

(copyreg.__newobj__, type(self), self.__dict__, iter(self), None)例如,必须使用自定义版本.这确实带来了一些额外的开销((type(self), (tuple(self), self.option), None, None, None)在酸洗和去除时会有额外的内存).