【问题标题】:Python: Pickling a dict with some unpicklable itemsPython:用一些不可腌制的物品腌制一个字典
【发布时间】:2011-05-04 02:25:17
【问题描述】:

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

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

我想腌制这个gui_project,连同命名空间。问题是,命名空间中的某些对象(即.namespace dict 的值)不是可腌制对象。例如,其中一些引用 wxPython 小部件。

我想过滤掉不可腌制的对象,即从腌制版本中排除它们。

我该怎么做?

(我尝试过的一件事是逐个处理值并尝试腌制它们,但是发生了一些无限递归,我需要避免这种情况。)

(我现在确实实现了GuiProject.__getstate__ 方法,以摆脱除namespace 之外的其他不可取的东西。)

【问题讨论】:

    标签: python pickle graceful-degradation


    【解决方案1】:

    我会使用pickler 记录的对持久对象引用的支持。持久对象引用是pickle引用但不存储在pickle中的对象。

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

    ZODB 已使用此 API 多年,因此非常稳定。 unpickling 时,您可以将对象引用替换为您喜欢的任何内容。在您的情况下,您可能希望将对象引用替换为指示无法腌制对象的标记。

    你可以从这样的东西开始(未经测试):

    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
    

    【讨论】:

    • 是否可以使用此解决方案,但不使用自定义转储和加载功能,而是使用自定义Pickler 类?如果是这样,我是否还需要继承Unpickler?他们怎么知道一起工作,例如,如果有人试图使用股票 loads 来解开我的 Pickler 子类腌制的东西怎么办?
    • 你总是可以继承picklers和unpicklers,但是你处于无证领域。关于协同工作:如果您尝试使用不具有 persistent_load 函数的 unpickler 来取消包含持久性 ID 的内容,则会出现异常。
    • 我添加了一个使用包装类而不是独立函数或子类的示例。
    • 是否有可能在 GuiProject.__getstate__ 函数中找出哪个 Pickler 子类正在腌制我们,以断言它是我们的特殊腌制器?
    • 另外,你为什么封装PicklerUnpickler而不是子类化它们?对它们进行子类化有什么问题吗?
    【解决方案2】:

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

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

    现在,当你 unpickle 时,你会取回所有原本可以 pickle 的变量。对于所有不可腌制的变量,您现在有一个字符串列表(合法的 Python 代码),当按顺序执行时,会为您提供所需的变量。

    希望对你有帮助

    【讨论】:

      【解决方案3】:

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

      Here's the code。 (查找CutePicklerCuteUnpickler。)Here are the tests。它是GarlicSim 的一部分,因此您可以通过installing garlicsimfrom garlicsim.general_misc import pickle_tools 使用它。

      如果您想在 Python 3 代码中使用它,请使用 Python 3 fork of garlicsim

      【讨论】:

      • 也许你应该把 pickle 部分放到一个单独的模块中以便于重用(虽然,看起来 pickle_tools 实际上确实使用了 general_misc 中的很多内容)。此外,它仍然在(某些)function 对象上失败。
      【解决方案4】:

      一种方法是从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(我不仅要针对不同的版本而且要针对不同的实现来验证它)这一事实之外,我不想要求想要腌制 gui_project 的人使用像这样的自定义pickler。如果我没有更好的选择,我会采用这个解决方案。
      【解决方案5】:

      过滤部分确实很棘手。使用简单的技巧,您可以轻松地让泡菜发挥作用。但是,当过滤器看起来更深一点时,您最终可能会过滤掉太多并丢失您可以保留的信息。但是,.namespace 中出现的事情的巨大可能性使得构建一个好的过滤器变得困难。

      但是,我们可以利用已经是 Python 一部分的部分,例如 copy 模块中的 deepcopy

      我复制了 stock copy 模块,做了以下事情:

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

      以下是差异:

      [~/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) unpicklable 文件对象被转换为LostObject 实例; 3) 不会创建大对象的新副本,因为它是可腌制的。

      【讨论】:

      • 这会涉及到.namespace 中对象的实际深度复制吗?我的意思是,用户可能在那里有巨大的对象,我不想复制它们。
      • 我刚刚进行了更改以提高内存效率。如果对象是可腌制的,则不会制作真正的副本。如果它不可腌制,程序会递归地为其可腌制部分制作浅拷贝。例如,一个包含可挑选列表和不可挑选原子对象的 2 元组本身是不可挑选的。要复制它,新的 deepcopy 函数将创建一个新的 2 元组,其第一个元素将指向同一个可提取列表,其第二个元素将是一个 LostObject 实例。不会创建列表的副本。现在 2 元组是可腌制的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-08-18
      • 1970-01-01
      • 1970-01-01
      • 2016-02-21
      • 2012-11-21
      • 2018-03-12
      • 2013-05-02
      相关资源
      最近更新 更多