Python: 如何对包含无法序列化项目的字典进行Pickling?

12

我有一个对象gui_project,它有一个属性.namespace,它是一种命名空间字典(即从字符串到对象的字典)。

(这在类似IDE的程序中用于让用户在Python shell中定义自己的对象。)

我想将这个gui_project与命名空间一起pickle起来。问题在于,命名空间中的某些对象(即.namespace字典的值)不是可pickle的对象。例如,其中一些引用了wxPython小部件。

我想过滤掉不可pickle的对象,也就是将它们从pickle版本中排除。

我该怎么做?

(我尝试了逐个对这些值进行pickle,但发生了一些无限递归的情况,我需要避免这种情况。)

(我现在实现了一个GuiProject.__getstate__方法,以除去除namespace之外的其他不可pickle的东西。)

5个回答

7
我会使用pickler文档中为持久化对象引用提供的支持。持久化对象引用是由pickle引用但未存储在pickle中的对象。

http://docs.python.org/library/pickle.html#pickling-and-unpickling-external-objects

ZODB多年来一直使用这个API,所以它非常稳定。在反序列化时,您可以将对象引用替换为任何您喜欢的内容。在您的情况下,您需要用标记替换对象引用,指示无法对这些对象进行序列化。

您可以从以下内容开始(未经测试):

import cPickle

def persistent_id(obj):
    if isinstance(obj, wxObject):
        return "filtered:wxObject"
    else:
        return None

class FilteredObject:
    def __init__(self, about):
        self.about = about
    def __repr__(self):
        return 'FilteredObject(%s)' % repr(self.about)

def persistent_load(obj_id):
    if obj_id.startswith('filtered:'):
        return FilteredObject(obj_id[9:])
    else:
        raise cPickle.UnpicklingError('Invalid persistent id')

def dump_filtered(obj, file):
    p = cPickle.Pickler(file)
    p.persistent_id = persistent_id
    p.dump(obj)

def load_filtered(file)
    u = cPickle.Unpickler(file)
    u.persistent_load = persistent_load
    return u.load()

然后只需调用dump_filtered()和load_filtered(),而不是pickle.dump()和pickle.load()。wxPython对象将被拾取为持久ID,在取消拾取时将被替换为FilteredObjects。
您可以通过过滤掉不属于内置类型且没有__getstate__方法的对象来使解决方案更加通用。
更新(2010年11月15日):这里有一种使用包装器类实现相同效果的方法。使用包装器类而不是子类,可以在文档API内保持。
from cPickle import Pickler, Unpickler, UnpicklingError


class FilteredObject:
    def __init__(self, about):
        self.about = about
    def __repr__(self):
        return 'FilteredObject(%s)' % repr(self.about)


class MyPickler(object):

    def __init__(self, file, protocol=0):
        pickler = Pickler(file, protocol)
        pickler.persistent_id = self.persistent_id
        self.dump = pickler.dump
        self.clear_memo = pickler.clear_memo

    def persistent_id(self, obj):
        if not hasattr(obj, '__getstate__') and not isinstance(obj,
            (basestring, int, long, float, tuple, list, set, dict)):
            return "filtered:%s" % type(obj)
        else:
            return None


class MyUnpickler(object):

    def __init__(self, file):
        unpickler = Unpickler(file)
        unpickler.persistent_load = self.persistent_load
        self.load = unpickler.load
        self.noload = unpickler.noload

    def persistent_load(self, obj_id):
        if obj_id.startswith('filtered:'):
            return FilteredObject(obj_id[9:])
        else:
            raise UnpicklingError('Invalid persistent id')


if __name__ == '__main__':
    from cStringIO import StringIO

    class UnpickleableThing(object):
        pass

    f = StringIO()
    p = MyPickler(f)
    p.dump({'a': 1, 'b': UnpickleableThing()})

    f.seek(0)
    u = MyUnpickler(f)
    obj = u.load()
    print obj

    assert obj['a'] == 1
    assert isinstance(obj['b'], FilteredObject)
    assert obj['b'].about

1
能否使用这个解决方案,但不使用自定义的转储和加载函数,而是使用自定义的Pickler类?如果可以,我还需要子类化Unpickler吗?它们如何知道如何协同工作,例如,如果有人尝试使用标准的loads来反序列化由我的Pickler子类pickle的内容,会发生什么? - Ram Rachum
你可以随时子类化picklers和unpicklers,但这样你就处于未记录的领域。关于协同工作:如果您尝试使用没有persistent_load函数的unpickler来反序列化包含持久性ID的内容,则会引发异常。 - Shane Hathaway
我添加了一个示例,使用包装类而不是独立的函数或子类。 - Shane Hathaway
GuiProject.__getstate__ 函数内部,是否有可能找出正在对我们进行 pickling 的 Pickler 子类,以便断言它是我们的特殊 pickler? - Ram Rachum
另外,为什么你封装了 PicklerUnpickler 而不是继承它们?继承它们有什么问题吗? - Ram Rachum
显示剩余2条评论

1

最终我用了Shane Hathaway的方法编写了自己的解决方案。

这是代码。(查找CutePicklerCuteUnpickler。)这是测试。这是GarlicSim的一部分,所以您可以通过安装garlicsim并执行from garlicsim.general_misc import pickle_tools来使用它。

如果您想在Python 3代码上使用它,请使用garlicsim Python 3分支


1
也许你应该将那个pickle部分制作成一个单独的模块,以便更容易地重用(尽管似乎pickle_tools确实使用了general_misc中的很多内容)。此外,它仍然无法处理(某些)'function'对象。 - HoverHell

1

这是我会这样做的(我之前做过类似的事情并且成功了):

  1. 编写一个函数来确定一个对象是否可被pickle
  2. 根据上述函数,列出所有可被pickle的变量列表
  3. 创建一个新的字典(称为D),用于存储所有不可被pickle的变量
  4. 对于D中的每个变量(仅在d中有非常相似的变量时才有效),制作一个字符串列表,其中每个字符串都是合法的Python代码,当按顺序执行所有这些字符串时,您将获得所需的变量

现在,当您进行反pickle操作时,您将获得最初可被pickle的所有变量。对于所有不可被pickle的变量,您现在拥有一个字符串列表(合法的Python代码),当按顺序执行时,可以得到所需的变量。

希望这可以帮助到您。


0
一种方法是从 pickle.Pickler 继承,并覆盖 save_dict() 方法。从基类中复制它,其内容如下:
def save_dict(self, obj):
    write = self.write

    if self.bin:
        write(EMPTY_DICT)
    else:   # proto 0 -- can't use EMPTY_DICT
        write(MARK + DICT)

    self.memoize(obj)
    self._batch_setitems(obj.iteritems())

然而,在_batch_setitems中,传递一个迭代器来过滤掉所有不想被转储的项,例如。
def save_dict(self, obj):
    write = self.write

    if self.bin:
        write(EMPTY_DICT)
    else:   # proto 0 -- can't use EMPTY_DICT
        write(MARK + DICT)

    self.memoize(obj)
    self._batch_setitems(item for item in obj.iteritems() 
                         if not isinstance(item[1], bad_type))

由于save_dict不是官方API,您需要检查每个新的Python版本是否仍然正确覆盖。


嗯,有更便携的解决方案吗?除了 save_dict 不是官方 API(我不仅需要验证不同版本,还需要验证不同实现),我也不想要求想要 pickle gui_project 的人使用这样的自定义 pickler。如果没有更好的选择,我会采用这个解决方案。 - Ram Rachum

0

过滤部分确实很棘手。使用简单的技巧,您可以轻松地让pickle工作。然而,当过滤器深入一点时,您可能会过滤掉太多内容并且失去了本来可以保留的信息。但是,可能出现在.namespace中的各种可能性使得构建一个好的过滤器变得困难。

然而,我们可以利用已经存在于Python中的部分内容,例如copy模块中的deepcopy。

我复制了原始的copy模块,并进行了以下操作:

  1. 创建一个名为LostObject的新类型,用于表示在pickling过程中将丢失的对象。
  2. 更改_deepcopy_atomic以确保x是可picklable的。如果不是,则返回LostObject的实例。
  3. 对象可以定义方法__reduce__和/或__reduce_ex__来提供关于是否以及如何pickle它的提示。我们确保这些方法不会抛出异常,以提供无法pickle的提示。
  4. 为了避免不必要地复制大型对象(类似于实际的深度复制),我们递归检查对象是否可pickle,并仅制作不可pickle的部分。例如,对于包含可pickle列表和不可pickle对象的元组,我们将复制元组 - 仅容器 - 而不是其成员列表。

以下是差异:

[~/Development/scratch/] $ diff -uN  /System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/copy.py mcopy.py
--- /System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/copy.py  2010-01-09 00:18:38.000000000 -0800
+++ mcopy.py    2010-11-10 08:50:26.000000000 -0800
@@ -157,6 +157,13 @@

     cls = type(x)

+    # if x is picklable, there is no need to make a new copy, just ref it
+    try:
+        dumps(x)
+        return x
+    except TypeError:
+        pass
+
     copier = _deepcopy_dispatch.get(cls)
     if copier:
         y = copier(x, memo)
@@ -179,10 +186,18 @@
                     reductor = getattr(x, "__reduce_ex__", None)
                     if reductor:
                         rv = reductor(2)
+                        try:
+                            x.__reduce_ex__()
+                        except TypeError:
+                            rv = LostObject, tuple()
                     else:
                         reductor = getattr(x, "__reduce__", None)
                         if reductor:
                             rv = reductor()
+                            try:
+                                x.__reduce__()
+                            except TypeError:
+                                rv = LostObject, tuple()
                         else:
                             raise Error(
                                 "un(deep)copyable object of type %s" % cls)
@@ -194,7 +209,12 @@

 _deepcopy_dispatch = d = {}

+from pickle import dumps
+class LostObject(object): pass
 def _deepcopy_atomic(x, memo):
+    try:
+        dumps(x)
+    except TypeError: return LostObject()
     return x
 d[type(None)] = _deepcopy_atomic
 d[type(Ellipsis)] = _deepcopy_atomic

现在回到腌制部分。您只需使用这个新的deepcopy函数进行深拷贝,然后将副本进行腌制。在复制过程中,无法腌制的部分已被删除。
x = dict(a=1)
xx = dict(x=x)
x['xx'] = xx
x['f'] = file('/tmp/1', 'w')
class List():
    def __init__(self, *args, **kwargs):
        print 'making a copy of a list'
        self.data = list(*args, **kwargs)
x['large'] = List(range(1000))
# now x contains a loop and a unpickable file object
# the following line will throw
from pickle import dumps, loads
try:
    dumps(x)
except TypeError:
    print 'yes, it throws'

def check_picklable(x):
    try:
        dumps(x)
    except TypeError:
        return False
    return True

class LostObject(object): pass

from mcopy import deepcopy

# though x has a big List object, this deepcopy will not make a new copy of it
c = deepcopy(x)
dumps(c)
cc = loads(dumps(c))
# check loop refrence
if cc['xx']['x'] == cc:
    print 'yes, loop reference is preserved'
# check unpickable part
if isinstance(cc['f'], LostObject):
    print 'unpicklable part is now an instance of LostObject'
# check large object
if loads(dumps(c))['large'].data[999] == x['large'].data[999]:
    print 'large object is ok'

这是输出结果:

making a copy of a list
yes, it throws
yes, loop reference is preserved
unpicklable part is now an instance of LostObject
large object is ok

你会发现:1)互相指针(即 xxx 之间的指针)得到了保留且不会进入无限循环;2)无法反序列化的文件对象被转换成了LostObject实例;3)由于该大型对象是可序列化的,因此不会创建新的副本。


这会涉及到在.namespace中实际进行对象的深度复制吗?我的意思是,用户可能会有巨大的对象,我不想复制它们。 - Ram Rachum
我刚刚进行了一些更改,以使它更具内存效率。如果一个对象是可picklable的,则不会真正创建副本。如果它不能被picklable,则程序会递归地为其picklable部分创建浅层副本。例如,一个包含可picklable列表和不可picklable原子对象本身的2元组不是可picklable的。为了复制它,新的“deepcopy”函数将创建一个新的2元组,其第一个元素将指向相同的可picklable列表,而第二个元素将是一个“LostObject”实例。不会创建列表的副本。现在,这个2元组是可picklable的。 - eddie_c

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接