Python: мариновать диктовку непобедимыми предметами
У меня есть объект gui_project
который имеет атрибут .namespace
, который является пространством имен dict. (то есть диктат от строк до объектов.)
(Это используется в IDE-подобной программе, чтобы позволить пользователю определять свой собственный объект в оболочке Python.)
Я хочу засолить это gui_project
вместе с пространством имен. Проблема в том, что некоторые объекты в пространстве имен (то есть значения .namespace
dict) не являются маринованными предметами. Например, некоторые из них ссылаются на виджеты wxPython.
Я бы хотел отфильтровать необратимые объекты, то есть исключить их из маринованной версии.
Как я могу это сделать?
(Одна вещь, которую я попытался, это перейти по одному к значениям и попытаться их перебрать, но произошла некоторая бесконечная рекурсия, и я должен быть в безопасности от этого.)
(Я реализую GuiProject.__getstate__
метод прямо сейчас, чтобы избавиться от других непростительных вещей, кроме namespace
.)
5 ответов
В итоге я написал свое собственное решение, используя подход Шейна Хэтэуэя.
Вот код(Ищу CutePickler
а также CuteUnpickler
.) Вот тесты. Это часть GarlicSim, так что вы можете использовать его, установивgarlicsim
и делать from garlicsim.general_misc import pickle_tools
,
Если вы хотите использовать его в коде Python 3, используйте форк Python 3garlicsim
,
Я бы использовал документированную поддержку выбора для постоянных ссылок на объекты. Постоянные ссылки на объекты - это объекты, на которые ссылается указатель, но которые не сохраняются в указателе.
http://docs.python.org/library/pickle.html
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 будут выбираться как постоянные идентификаторы, которые будут заменены на 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
Вот как я бы это сделал (я делал что-то подобное раньше, и это работало):
- Напишите функцию, которая определяет, является ли объект маринованным
- Составьте список всех выбираемых переменных, основываясь на приведенной выше функции
- Создайте новый словарь (называемый D), в котором будут храниться все непереборчивые переменные
- Для каждой переменной в D (это работает, только если у вас есть очень похожие переменные в d), составьте список строк, где каждая строка является допустимым кодом Python, так что, когда все эти строки выполняются по порядку, вы получаете желаемую переменную
Теперь, когда вы откручиваете данные, вы возвращаете все переменные, которые изначально могли быть использованы. Для всех переменных, которые нельзя было выбрать, теперь у вас есть список строк (допустимый код Python), который при выполнении по порядку дает вам желаемую переменную.
Надеюсь это поможет
Одним из подходов будет наследование от 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, является ли это переопределение все еще правильным.
Фильтрующая часть действительно хитрая. Используя простые приемы, вы можете легко заставить рассол работать. Однако вы можете отфильтровать слишком много и потерять информацию, которую можете сохранить, когда фильтр выглядит немного глубже. Но огромные возможности вещей, которые могут в конечном итоге в .namespace
затрудняет создание хорошего фильтра.
Тем не менее, мы могли бы использовать части, которые уже являются частью Python, такие как deepcopy
в copy
модуль.
Я сделал копию акции copy
модуль, и сделал следующие вещи:
- создать новый тип с именем
LostObject
представлять объект, который будет потерян при травлении. - менять
_deepcopy_atomic
Чтобы убедитьсяx
маринованные Если это не так, верните экземплярLostObject
- объекты могут определять методы
__reduce__
и / или__reduce_ex__
дать подсказку о том, стоит ли и как это мариновать. Мы уверены, что эти методы не будут генерировать исключение, чтобы дать подсказку, что оно не может быть обработано. - чтобы избежать создания ненужной копии большого объекта (например, глубокой копии), мы рекурсивно проверяем, является ли объект доступным для выбора, и делаем только необрабатываемую часть. Например, для кортежа из списка с возможностью выбора и без возможности выбора объекта мы сделаем копию кортежа - просто контейнера - но не его списка членов.
Ниже приводится разница:
[~/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) не создается новая копия большого объекта, так как он может быть выбран.