Сериализация RangeDict с использованием YAML или JSON в Python
Я использую RangeDict, чтобы сделать словарь, который содержит диапазоны. Когда я использую Pickle, он легко записывается в файл и позже читается.
import pickle
from rangedict import RangeDict
rngdct = RangeDict()
rngdct[(1, 9)] = \
{"Type": "A", "Series": "1"}
rngdct[(10, 19)] = \
{"Type": "B", "Series": "1"}
with open('rangedict.pickle', 'wb') as f:
pickle.dump(rngdct, f)
Тем не менее, я хочу использовать YAML (или JSON, если YAML не будет работать...) вместо Pickle, так как большинство людей, кажется, ненавидят это (и я хочу, чтобы файлы читались человеком, чтобы они имели смысл для людей, читающих их)
В основном, изменив код для вызова yaml и открыв файл в 'w'
режим, не в 'wb'
делает трюк для стороны записи, но когда я читаю файл в другом скрипте, я получаю эти ошибки:
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/yaml/constructor.py", line 129, in construct_mapping
value = self.construct_object(value_node, deep=deep)
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/yaml/constructor.py", line 61, in construct_object
"found unconstructable recursive node", node.start_mark)
yaml.constructor.ConstructorError: found unconstructable recursive node
Я потерян здесь. Как я могу сериализовать объект rangedict и прочитать его обратно в исходном виде?
1 ответ
TL;DR; Перейти к нижней части этого ответа для рабочего кода
Я уверен, что некоторые люди ненавидят pickle
Это, безусловно, может вызвать некоторые проблемы при рефакторинге кода (когда классы протравленных объектов перемещаются в разные файлы). Но большая проблема в том, что рассол небезопасен, просто YAML - это то, как вы его использовали.
Интересно отметить, что вы не можете выбрать более читаемый уровень протокола 0 (по умолчанию в Python 3 - версия протокола 3), так как:
pickle.dump (rngdct, f, protocol = 0) выдаст:
TypeError: класс, который определяет слоты без определения gettate, не может бытьвыбран
Это потому чтоRangeDict
module / class немного минималистичен, что также показывает (а точнее не показывает), если вы пытаетесь сделать:
print(rngdict)
который будет просто печатать{}
Вы, вероятно, использовали PyYAML dump()
рутина (и соответствующая, небезопасная, load()
). И хотя это может создавать дамп общих классов Python, вы должны понимать, что это было реализовано до или примерно одновременно с Python 3.0. (и поддержка Python 3 была реализована позже). И хотя нет никаких причин, по которым YAML-парсер мог бы сбрасывать и загружать точную информацию, котораяpickle
делает, это не зацепитьpickle
подпрограммы поддержки (хотя это возможно) и, конечно, не в информации для конкретных протоколов протравливания Python 3.
В любом случае, без конкретного представителя (и конструктора) дляRangeDict
объекты, использование YAML на самом деле не имеет никакого смысла: это делает загрузку потенциально небезопасной, а ваш YAML включает в себя все детали, которые делают объект эффективным. Если вы делаетеyaml.dump()
:
!!python/object:rangedict.RangeDict
_root: &id001 !!python/object/new:rangedict.Node
state: !!python/tuple
- null
- color: 0
left: null
parent: null
r: !!python/tuple [1, 9]
right: !!python/object/new:rangedict.Node
state: !!python/tuple
- null
- color: 1
left: null
parent: *id001
r: !!python/tuple [10, 19]
right: null
value: {Series: '1', Type: B}
value: {Series: '1', Type: A}
Где IMOчитаемое представление в YAML будет:
!rangedict
[1, 9]:
Type: A
Series: '1'
[10, 19]:
Type: B
Series: '1'
Из-за последовательностей, используемых в качестве ключей, PyYAML не может загрузить их без значительных изменений синтаксического анализатора. Но, к счастью, эти модификации были включены вruamel.yaml
(отказ от ответственности: я являюсь автором этого пакета), поэтому "все", что вам нужно сделать, это подкласс RangeDict
обеспечить подходящие методы представления и конструктора (класса):
import io
import ruamel.yaml
from rangedict import RangeDict
class MyRangeDict(RangeDict):
yaml_tag = u'!rangedict'
def _walk(self, cur):
# walk tree left -> parent -> right
if cur.left:
for x in self._walk(cur.left):
yield x
yield cur.r
if cur.right:
for x in self._walk(cur.right):
yield x
@classmethod
def to_yaml(cls, representer, node):
d = ruamel.yaml.comments.CommentedMap()
for x in node._walk(node._root):
d[ruamel.yaml.comments.CommentedKeySeq(x)] = node[x[0]]
return representer.represent_mapping(cls.yaml_tag, d)
@classmethod
def from_yaml(cls, constructor, node):
d = cls()
for x, y in node.value:
x = constructor.construct_object(x, deep=True)
y = constructor.construct_object(y, deep=True)
d[x] = y
return d
rngdct = MyRangeDict()
rngdct[(1, 9)] = \
{"Type": "A", "Series": "1"}
rngdct[(10, 19)] = \
{"Type": "B", "Series": "1"}
yaml = ruamel.yaml.YAML()
yaml.register_class(MyRangeDict) # tell the yaml instance about this class
buf = io.StringIO()
yaml.dump(rngdct, buf)
data = yaml.load(buf.getvalue())
# test for round-trip equivalence:
for x in data._walk(data._root):
for y in range(x[0], x[1]+1):
assert data[y]['Type'] == rngdct[y]['Type']
assert data[y]['Series'] == rngdct[y]['Series']
buf.getvalue()
это точно читаемое представление, показанное ранее.
Если вам приходится иметь дело с демпингом RangeDict
сам (то есть не может подкласс, потому что вы используете некоторую библиотеку, которая имеет RangeDict
жестко закодировано), то вы можете добавить атрибут и методы MyRangeDict
прямо к RangeDict
прививая / обезьяна патчинг.