Python: le Décapage d'un dict avec certains éléments unpicklable

j'ai un objet gui_project qui a un attribut .namespace , qui est un espace de noms dict. (c'est à dire une dict à partir de chaînes à des objets.)

(utilisé dans un programme semblable à un IDE pour permettre à l'utilisateur de définir son propre objet dans un shell Python.)

je veux choisir ce gui_project , ainsi que l'Espace-nom. Le problème est que certains objets dans l'espace de noms (c'est-à-dire les valeurs du dict .namespace ) ne sont pas des objets sélectionnables. Par exemple, certains d'entre eux se référer aux widgets wxPython.

j'aimerais filtrer les objets impraticables, c'est-à-dire les exclure de la version pickled.

Comment faire?

(une chose que j'ai essayé est d'aller un par un sur les valeurs et d'essayer de les pickle, mais une certaine récursion infinie s'est produite, et je dois être à l'abri de cela.)

(Je ne mettre en œuvre une méthode GuiProject.__getstate__ en ce moment, pour se débarrasser d'autres choses impraticables en plus namespace .)

11
demandé sur Ram Rachum 2010-11-02 21:04:49

5 réponses

j'utiliserais le support documenté de pickler pour les références d'objets persistants. Objet persistant références sont des objets qui sont référencés par la saumure, mais non conservés dans la saumure.

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

ZODB utilise cette API depuis des années, elle est donc très stable. Lors de la décompression, vous pouvez remplacer les références de l'objet par tout ce que vous voulez. Dans votre cas, vous voudriez remplacer les références d'objet par des marqueurs indiquant que les objets ne pouvaient pas être décapés.

vous pourriez commencer par quelque chose comme ceci (non testé):

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()

alors il suffit d'appeler dump_filtered() et load_filtered () au lieu de pickle.dump() et de la saumure.charge.)( les objets wxPython seront remplacés par des objets FilteredObjects au moment du déballage.

vous pourriez faire le solution plus générique en filtrant les objets qui ne sont pas des types intégrés et qui n'ont pas de méthode __getstate__ .

Update (15 Nov 2010): voici une façon d'obtenir la même chose avec les classes d'emballage. En utilisant les classes wrapper au lieu des sous-classes, il est possible de rester dans l'API documentée.

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
6
répondu Shane Hathaway 2010-11-15 20:51:26

C'est comme ça que je ferais ceci (j'ai fait quelque chose de similaire avant et ça a fonctionné):

  1. écrire une fonction qui détermine si un objet est décapable ou non
  2. faire une liste de toutes les variables de sélection, basé sur la fonction ci-dessus
  3. faire un nouveau dictionnaire (appelé D) qui stocke toutes les variables non-pickleable
  4. pour chaque variable en D (cela ne fonctionne que si vous avez des variables d) faites une liste des chaînes, où chaque chaîne est du code python légal, tel que quand toutes ces chaînes sont exécutées dans l'ordre, vous obtenez la variable désirée

maintenant, quand vous unpickle, vous obtenez en arrière toutes les variables qui étaient à l'origine pickleable. Pour toutes les variables qui n'étaient pas sélectionnables, vous avez maintenant une liste de chaînes (code python légal) qui, lorsqu'elles sont exécutées dans l'ordre, vous donne la variable désirée.

Espérons que cette aide

1
répondu inspectorG4dget 2010-11-15 20:55:49

j'ai fini par coder ma propre solution, en utilisant L'approche de Shane Hathaway.

voici le code . (Cherchez CutePickler et CuteUnpickler .) Voici les tests . Il fait partie de GarlicSim , donc vous pouvez l'utiliser par installation garlicsim et faire from garlicsim.general_misc import pickle_tools .

si vous voulez l'utiliser sur le code Python 3, Utilisez le Python 3 fourche de garlicsim .

1
répondu Ram Rachum 2010-12-24 11:51:41

une approche serait d'hériter de pickle.Pickler , et de remplacer la méthode save_dict() . Copiez-le de la classe de base, qui lit comme ceci:

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())

cependant, dans les _batch_setitems, Passez un itérateur qui filtre tous les articles que vous ne voulez pas jeter, E. g

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))

comme save_dict n'est pas une API officielle, vous devez vérifier pour chaque nouvelle version de Python si cette option est toujours correcte.

0
répondu Martin v. Löwis 2010-11-02 18:35:20

la partie filtrante est en effet délicate. En utilisant des trucs simples, vous pouvez facilement obtenir le cornichon à travailler. Cependant, vous pourriez finir par filtrer trop et perdre des informations que vous pourriez garder lorsque le filtre semble un peu plus profond. Mais la vaste possibilité de choses qui peuvent finir dans le .namespace rend difficile la construction d'un bon filtre.

Cependant, nous pourrions tirer parti de pièces qui font déjà partie de Python, comme deepcopy dans le copy module.

j'ai fait une copie du stock copy module, et fait les choses suivantes:

  1. créer un nouveau type appelé LostObject pour représenter un objet qui sera perdu dans pickling.
  2. modifier _deepcopy_atomic pour s'assurer que x est sélectionnable. Si ce n'est pas le cas, retournez une instance de LostObject
  3. Les objets
  4. peuvent définir les méthodes __reduce__ et / ou __reduce_ex__ pour fournir une indication si, et comment, à mariner. Nous nous assurons que ces méthodes ne vont pas jeter l'exception pour fournir une indication qu'il ne peut pas être décapé.
  5. pour éviter de faire une copie inutile du grand objet ( a la deepcopy réelle), nous vérifions récursivement si un objet est picklable, et ne faire partie unpicklable. Par exemple, pour un tuple d'une liste de sélection et d'un objet non cliquable, nous ferons une copie du tuple-juste le conteneur - mais pas son membre liste.

ce qui suit est la diff:

[~/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

maintenant retour à la partie de décapage. Il vous suffit de faire une copie en profondeur en utilisant cette nouvelle fonction deepcopy et ensuite de décaper la copie. Les parties impraticables ont été enlevées pendant le processus de copie.

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'

Voici la sortie:

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

vous voyez que 1) les indicateurs mutuels (entre x et xx ) sont préservé et nous ne courons pas en boucle infinie; 2) l'objet fichier non cliquable est converti en une instance LostObject ; et 3) Pas de nouvelle copie du grand objet est créé car il est sélectionnable.

0
répondu eddie_c 2010-11-10 17:08:40