Подтвердить что ты не робот

Python: травление дикта с некоторыми неприступными вещами

У меня есть объект gui_project, который имеет атрибут .namespace, который является пространством имен dict. (т.е. dict от строк к объектам.)

(Это используется в IDE-подобной программе, чтобы пользователь мог определить свой собственный объект в оболочке Python.)

Я хочу раскрыть этот gui_project вместе с пространством имен. Проблема в том, что некоторые объекты в пространстве имен (т.е. Значения .namespace dict) не являются сортируемыми объектами. Например, некоторые из них относятся к виджетам wxPython.

Я хотел бы отфильтровать неприводимые объекты, то есть исключить их из маринованной версии.

Как я могу это сделать?

(Одна вещь, которую я пробовал - это идти один за другим по значениям и пытаться рассолить их, но произошла бесконечная рекурсия, и я должен быть в безопасности от этого.)

(сейчас я реализую метод GuiProject.__getstate__, чтобы избавиться от других непривычных вещей, кроме namespace.)

4b9b3361

Ответ 1

Я закончил кодирование своего собственного решения, используя подход Шейна Хэтэуэй.

Здесь код. (Ищите CutePickler и CuteUnpickler.) Вот тесты. Это часть GarlicSim, поэтому вы можете использовать ее установка garlicsim и выполните from garlicsim.general_misc import pickle_tools.

Если вы хотите использовать его в коде Python 3, используйте вилку Python 3 garlicsim.

Ответ 2

Я бы использовал документированную поддержку pickler для постоянных ссылок на объекты. Стойкие ссылки на объекты - это объекты, на которые ссылается рассол, но не хранятся в рассоле.

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

ZODB использует этот API уже много лет, поэтому он очень стабилен. При рассыпании вы можете заменить ссылки на объекты тем, что вам нравится. В вашем случае вы захотите заменить ссылки на объекты маркерами, указав, что объекты не могут быть маринованными.

Вы можете начать с чего-то вроде этого (untested):

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 будут мариноваться как постоянные идентификаторы, которые будут заменены на FilteredObjects при расписании.

Вы можете сделать решение более общим, отфильтровывая объекты, которые не являются встроенными, и не имеют метода __getstate__.

Обновление (15 ноября 2010 г.): Здесь вы можете сделать то же самое с классами-оболочками. Используя классы-оболочки вместо подклассов, можно остаться в документально подтвержденном 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

Ответ 3

Вот как бы я это сделал (раньше я делал что-то подобное и работал):

  • Напишите функцию, определяющую, является ли объект разборным.
  • Составьте список всех выбранных переменных, основанных на вышеуказанной функции
  • Создайте новый словарь (называемый D), в котором хранятся все несогласованные переменные
  • Для каждой переменной в D (это работает только в том случае, если у вас очень похожие переменные в d)  составить список строк, где каждая строка является легальным кодом на питоне, так что  когда все эти строки выполняются по порядку, вы получаете требуемую переменную

Теперь, когда вы распаковываете, вы возвращаете все переменные, которые были первоначально разборчивы. Для всех переменных, которые не были определены, теперь у вас есть список строк (легальный код python), который при выполнении по порядку дает желаемую переменную.

Надеюсь, что это поможет

Ответ 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, является ли это переопределение по-прежнему правильным.

Ответ 5

Фильтрационная часть действительно сложна. Используя простые трюки, вы можете легко заставить рассол работать. Тем не менее, вы можете в конечном итоге отфильтровать слишком много и потерять информацию, которую вы можете сохранить, когда фильтр выглядит немного глубже. Но огромная возможность вещей, которые могут оказаться в .namespace, затрудняет создание хорошего фильтра.

Однако мы могли бы использовать куски, которые уже являются частью Python, например deepcopy в модуле copy.

Я сделал копию запаса copy модуля и сделал следующие вещи:

  • создайте новый тип с именем LostObject для представления объекта, который будет потерян при травлении.
  • измените _deepcopy_atomic, чтобы удостовериться, что x выбрано. Если это не так, верните экземпляр LostObject
  • объекты могут определять методы __reduce__ и/или __reduce_ex__, чтобы дать подсказку о том, как и как их рассортировать. Мы убеждаемся, что эти методы не будут генерировать исключение, чтобы дать намек на то, что он не может быть маринован.
  • чтобы избежать ненужной копии большого объекта (a la actual deepcopy), мы рекурсивно проверяем, является ли объект разборчивым, и только сделать непригладимую часть. Например, для кортежа списка сортировки и неполибельного объекта мы сделаем копию кортежа - только контейнер, но не список его членов.

Ниже приведен 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

Теперь вернемся к травильной части. Вы просто делаете глубокую копию с помощью этой новой функции 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) взаимные указатели (между x и xx) сохраняются, и мы не сталкиваемся с бесконечным циклом; 2) неприменимый файл-объект преобразуется в экземпляр LostObject; и 3) не создается новая копия большого объекта, так как она является picklable.