如何检查复杂对象中无法被序列化的细节

22 投票
1 回答
4618 浏览
提问于 2025-04-17 21:10

概述

我想把我的复杂对象进行序列化。乍一看这很简单,但每一步都会出现不同的问题。

最终,其他程序员也应该能够创建一个从我的父对象继承的复杂对象。而且这个对象应该可以被pickle化,适用于Python 2.7和Python 3.x。

我从一个简单的对象开始,成功使用了pickle.dumppickle.load

然后我创建了多个复杂对象(相似但不完全相同),其中一些可以被序列化,而有些则不行。

调试

pickle库知道哪些对象可以被序列化,哪些不可以。从理论上讲,这意味着pdb可以被定制来启用pickle调试。

替代序列化库

我想要一个可靠的序列化工具,不受对象内容的影响。所以我搜索了其他序列化工具:

  • Cerealizer,自测失败,看起来已经过时。
  • MessagePack,但不支持Python 3。
  • 我尝试了JSON,结果出现错误: builtins.TypeError: <lib.scan.Content object at 0x7f37f1e5da50> is not JSON serializable
  • 我查看了Marshal和Shelve,但它们都提到pickle。

深入使用pickle

我读过如何检查对象是否可被pickle化,但没有得到答案。

我找到的最接近的内容是如何找到Python Pickle在大型对象中的错误来源

我对此进行了调整:

import pickle

if _future_.isPython3():        
    class MyPickler(pickle._Pickler):        
        def save(self, obj):             
            try:
                pickle._Pickler.save(self, obj)
            except:
                print ('pick(3.x) {0} of type {1}'.format(obj, type(obj)))                  
else:
    class MyPickler (pickle.Pickler):

        def save(self, obj):         
            try:
                pickle.Pickler.save(self, obj)
            except:
                print('pick(2.x)', obj, 'of type', type(obj))

我通过以下方式调用这段代码:

def save(obj, file):  
    if platform.python_implementation() == 'CPython':
        myPickler = MyPickler(file)                
        myPickler.save(obj) 

我希望保存操作能执行到抛出异常为止。obj的内容会被打印出来,这样我就能准确看到错误发生的位置。但结果是:

pick(3.x)  <class 'module'> of type <class 'type'>
pick(3.x)  <class 'module'> of type <class 'type'>
pick(3.x)  <class 'Struct'> of type <class 'type'>
pick(3.x)  <class 'site.setquit.<locals>.Quitter'> of type <class 'type'>
pick(3.x)  <class 'site.setquit.<locals>.Quitter'> of type <class 'type'>
pick(3.x)  <class 'module'> of type <class 'type'>
pick(3.x)  <class 'sys.int_info'> of type <class 'type'>
...

这只是结果的一小部分。我看不懂这个。它没有告诉我哪个细节在序列化时出错了,也不知道该如何解决。

我看过:http://docs.python.org/3/library/pickle.html#what-can-be-pickled-and-unpickled,但如果我无法检测到代码中的哪一行不能被pickle化,这对我帮助不大。

我复杂对象中的代码按预期工作,最终运行生成的代码如下:

sys.modules['unum']

但在序列化时,似乎“模块”没有按预期读取。

解决方案尝试

为了让大家明白我的意思,给点背景。我曾经有些程序正常工作,突然就不行了。可能是更新或其他资源的变化。对别人有效的程序对我却不行,反之亦然。

这是一个普遍的问题,所以我想开发一个程序来检查各种资源。不同类型的资源数量庞大。因此,我有一个父对象类,包含所有通用行为。还有一个尽可能小的细节类,用于特定资源。

这些在我的子资源类中完成。

这些资源需要用不同版本进行检查,比如Python 2.7或Python 3.3。如果你在Python 2.7.5下运行,资源是有效的,但如果需要Python 2.7及更高版本,就不行。所以检查必须比简单的相等值更复杂。

这在自定义配置文件中以单个语句指定。每个程序都有一个特定的配置文件,尽量保持简洁。一个资源在配置文件中用单个语句进行检查。

通用类大约占代码的98%。特定资源和配置仅占约2%的代码。因此,添加新的资源进行检查,以及为新程序创建新的配置文件非常简单。

这些子资源:

class R_Sys(r_base.R_Base):
    '''
    doc : http://docs.python.org/3/library/sys.html#module-sys

    sys.modules returns only a list of imported module

    statement :
    sys.modules['psutil'] #  may return false (installed but not imported
    but the statements :
    import psutil
    sys.modules['psutil'] # will return true, now psutil is imported
    '''

    allowed_names = ('modules', 'path', 'builtin_module_names', 'stdin')

    allowed_keys_in_dict_config = ('name',)
    allowed_operators = ("R_NONE", "=", 'installed')  # installed only for modules

    class_group = 'Sys'
    module_used = sys   


    def __init__(self, check_type, group, name):
        super(R_Sys, self).__init__(check_type, group, name)

通过这个配置语句调用:

sc.analyse(r.R_Sys, c.ct('DETECT'), dict(name='path'))

可以成功被pickle化。但使用配置语句:

sc.analyse(r.R_Sys, c.ct('DETECT'),
                     dict(name='modules', tuplename='unum') )  

则失败了。

在我看来,这意味着98%的主代码应该是没问题的,否则第一个语句也会失败。

子类中有类属性。这些属性是正常运行所必需的。而且在第一次调用时,序列化执行得很好。我还没有进行反序列化。

1 个回答

11

dill 有一些很不错的工具可以帮助我们检查对象的序列化(也就是“腌制”),其中最好的就是“腌制追踪”,这和你实现的功能类似。

我们来创建一个复杂的对象,看看具体情况:

>>> import dill
>>> class Foo(object):
...   @classmethod
...   def bar(self, x):
...     return self.z + x
...   def baz(self, z):
...     self.z = z
...   z = 1
...   zap = lambda self, x: x + self.bar(x)
... 
>>> f = Foo()
>>> f.zap(3)
7
>>> f.baz(7)
>>> f.z 
7

先开启“腌制追踪”:

>>> dill.detect.trace(True)
>>> _f = dill.dumps(f)
T2: <class '__main__.Foo'>
F2: <function _create_type at 0x10f94a668>
T1: <type 'type'>
F2: <function _load_type at 0x10f94a5f0>
T1: <type 'object'>
D2: <dict object at 0x10f96bb40>
Cm: <classmethod object at 0x10f9ad408>
T4: <type 'classmethod'>
F1: <function bar at 0x10f9aa9b0>
F2: <function _create_function at 0x10f94a6e0>
Co: <code object bar at 0x10f9a9130, file "<stdin>", line 2>
F2: <function _unmarshal at 0x10f94a578>
D1: <dict object at 0x10e8d6168>
D2: <dict object at 0x10f96b5c8>
F1: <function baz at 0x10f9aaa28>
Co: <code object baz at 0x10f9a9ab0, file "<stdin>", line 5>
D1: <dict object at 0x10e8d6168>
D2: <dict object at 0x10f969d70>
F1: <function <lambda> at 0x10f9aaaa0>
Co: <code object <lambda> at 0x10f9a9c30, file "<stdin>", line 8>
D1: <dict object at 0x10e8d6168>
D2: <dict object at 0x10f97d050>
D2: <dict object at 0x10e97b4b0>
>>> f_ = dill.loads(_f)
>>> f_.z
7

好吧,dill 能够对这个对象进行腌制……那我们就来增加点难度。 首先我们关闭追踪。

>>> dill.detect.trace(False)
>>> 
>>> f.y = xrange(5)
>>> f.w = iter([1,2,3])
>>> 
>>> dill.pickles(f)
False

好的,现在 dill 失败了。那么是什么导致了这个失败呢? 如果我们深入研究一下我们的对象 f,就能找到所有无法腌制的对象。

>>> dill.detect.badtypes(f)
<class '__main__.Foo'>
>>> dill.detect.badtypes(f, depth=1)
{'__hash__': <type 'method-wrapper'>, '__setattr__': <type 'method-wrapper'>, '__reduce_ex__': <type 'builtin_function_or_method'>, 'baz': <type 'instancemethod'>, '__reduce__': <type 'builtin_function_or_method'>, '__str__': <type 'method-wrapper'>, '__format__': <type 'builtin_function_or_method'>, '__getattribute__': <type 'method-wrapper'>, 'zap': <type 'instancemethod'>, '__delattr__': <type 'method-wrapper'>, '__repr__': <type 'method-wrapper'>, 'w': <type 'listiterator'>, '__dict__': <type 'dict'>, '__sizeof__': <type 'builtin_function_or_method'>, '__init__': <type 'method-wrapper'>}
>>> dill.detect.badobjects(f, depth=1)
{'__hash__': <method-wrapper '__hash__' of Foo object at 0x10f9b0050>, '__setattr__': <method-wrapper '__setattr__' of Foo object at 0x10f9b0050>, '__reduce_ex__': <built-in method __reduce_ex__ of Foo object at 0x10f9b0050>, 'baz': <bound method Foo.baz of <__main__.Foo object at 0x10f9b0050>>, '__reduce__': <built-in method __reduce__ of Foo object at 0x10f9b0050>, '__str__': <method-wrapper '__str__' of Foo object at 0x10f9b0050>, '__format__': <built-in method __format__ of Foo object at 0x10f9b0050>, '__getattribute__': <method-wrapper '__getattribute__' of Foo object at 0x10f9b0050>, 'zap': <bound method Foo.<lambda> of <__main__.Foo object at 0x10f9b0050>>, '__delattr__': <method-wrapper '__delattr__' of Foo object at 0x10f9b0050>, '__repr__': <method-wrapper '__repr__' of Foo object at 0x10f9b0050>, 'w': <listiterator object at 0x10f9b0550>, '__dict__': {'y': xrange(5), 'z': 7, 'w': <listiterator object at 0x10f9b0550>}, '__sizeof__': <built-in method __sizeof__ of Foo object at 0x10f9b0050>, '__init__': <method-wrapper '__init__' of Foo object at 0x10f9b0050>}

嗯,这可真不少。当然,并不是所有这些对象都需要被序列化,才能让我们的对象成功序列化……不过至少有一个对象导致了失败。

我们自然要看看具体的失败信息…… 那么,抛出的错误是什么呢?也许这能给我们一些线索。

>>> dill.detect.errors(f)
PicklingError("Can't pickle <type 'listiterator'>: it's not found as __builtin__.listiterator",)

啊哈,listiterator 是个问题对象。让我们通过重新开启“追踪”来深入了解。

>>> dill.detect.trace(True)
>>> dill.pickles(f)
T2: <class '__main__.Foo'>
F2: <function _create_type at 0x10f94a668>
T1: <type 'type'>
F2: <function _load_type at 0x10f94a5f0>
T1: <type 'object'>
D2: <dict object at 0x10f9826e0>
Cm: <classmethod object at 0x10f9ad408>
T4: <type 'classmethod'>
F1: <function bar at 0x10f9aa9b0>
F2: <function _create_function at 0x10f94a6e0>
Co: <code object bar at 0x10f9a9130, file "<stdin>", line 2>
F2: <function _unmarshal at 0x10f94a578>
D1: <dict object at 0x10e8d6168>
D2: <dict object at 0x10f96b5c8>
F1: <function baz at 0x10f9aaa28>
Co: <code object baz at 0x10f9a9ab0, file "<stdin>", line 5>
D1: <dict object at 0x10e8d6168>
D2: <dict object at 0x10f969d70>
F1: <function <lambda> at 0x10f9aaaa0>
Co: <code object <lambda> at 0x10f9a9c30, file "<stdin>", line 8>
D1: <dict object at 0x10e8d6168>
D2: <dict object at 0x10f97d050>
D2: <dict object at 0x10e97b4b0>
Si: xrange(5)
F2: <function _eval_repr at 0x10f94acf8>
T4: <type 'listiterator'>
False

确实,它在 listiterator 这里停住了。不过,注意到(就在上面)xrange 是可以被腌制的。所以,我们来把 iter 替换成 xrange

>>> f.w = xrange(1,4)  
>>> dill.detect.trace(False)
>>> dill.pickles(f)
True
>>> 

现在我们的对象又可以被腌制了。

dill 还内置了许多其他的腌制检测工具,包括追踪哪个对象指向哪个对象的方法(这对调试递归腌制失败很有帮助)。

我相信 cloudpickle 也有一些类似于 dill 的腌制调试工具……但无论哪种情况,主要的工具和你所构建的功能是相似的。

撰写回答