Сериализация 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, не может бытьвыбран

Это потому чтоRangeDictmodule / 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 прививая / обезьяна патчинг.

Другие вопросы по тегам