Сериализация 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 и прочитать его в исходной форме?


person Rene Knuvers    schedule 01.10.2017    source источник
comment
Я думаю, что NSStock было опечаткой, если нет, добавьте его определение в свой пример.   -  person Anthon    schedule 03.10.2017
comment
Вот так! Извините за это, я переименовал переменные, но забыл эту. Спасибо за замечание! @Антон   -  person Rene Knuvers    schedule 05.10.2017


Ответы (1)


TL;ДР; Перейдите к нижней части этого ответа для рабочего кода


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

Интересно отметить, что вы не можете перейти к более читаемому уровень протокола 0 (по умолчанию в Python 3 используется протокол версии 3):

pickle.dump(rngdct, f, protocol=0) выдаст:

TypeError: класс, который определяет слоты без определения getstate, не может быть обработан.

Это связано с тем, что модуль/класс RangeDict немного минималистичный, что также отображается (или, скорее, не отображается), если вы пытаетесь сделать:

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 путем прививки/обезьяны.

person Anthon    schedule 03.10.2017
comment
Это действительно рабочий ответ. Ваша библиотека YAML безупречно справляется с поставленной задачей, а также дает хорошо читаемый человеком выходной файл. Часть эквивалентности туда и обратно немного сомнительна для меня, но она не попадает в исключения утверждений, поэтому я предполагаю, что мой RangeDict имеет правильный формат/данные? - person Rene Knuvers; 05.10.2017
comment
Эта часть эквивалентности зависит от некоторых внутренних компонентов, она просто проверяет все диапазоны и гарантирует, что созданный rngdct имеет то же значение для первого значения в этом диапазоне (x[0]), что и data. Просто чтобы убедиться, что вы не load() что-то сделали и не избавились от ошибки, а получили что-то совершенно отличное от того, с чего вы начали. - person Anthon; 05.10.2017