Класс синтаксического анализа Python из YAML

Я пытаюсь вывести, а затем проанализировать из YAML следующие

import numpy as np
class MyClass(object):
    YAMLTag = '!MyClass'

    def __init__(self, name, times, zeros):
        self.name   = name
        self._T     = np.array(times)
        self._zeros = np.array(zeros)

Файл YAML выглядит как

!MyClass:
  name: InstanceId
  times: [0.0, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0]
  zeros: [0.03, 0.03, 0.04, 0.03, 0.03, 0.02, 0.03]

Чтобы написать, я добавил в класс два метода

def toDict(self):
    return {'name'  : self.name,
            'times' : [float(t) for t in self._T],
            'zeros' : [float(t) for t in self._zeros]}
@staticmethod
def ToYAML(dumper, data):
    return dumper.represent_dict({data.YAMLTag : data.toDict()})

и читать, метод

@staticmethod
def FromYAML(loader, node):
    nodeMap = loader.construct_mapping(node)
    return MyClass(name  = nodeMap['name'],
                   times = nodeMap['times'],
                   zeros = nodeMap['zeros'])

и следуя документации YAML, я добавил следующий фрагмент в тот же файл Python myClass.py:

import yaml

yaml.add_constructor(MyClass.YAMLTag, MyClass.FromYAML)
yaml.add_representer(MyClass,         MyClass.ToYAML)

Кажется, что запись работает нормально, но при чтении YAML код

loader.construct_mapping(node)

кажется, возвращает словарь с пустыми данными:

{'zeros': [], 'name': 'InstanceId', 'times': []}

Как мне исправить читателя, чтобы он мог делать это правильно? Или, может быть, я что-то не выписываю правильно? Я потратил много времени на изучение документации PyYAML и отладку того, как реализован пакет, но не могу понять, как разобрать сложную структуру, и единственный пример, который я, кажется, нашел, имеет однострочный класс, который легко разбирается.


Связано: синтаксический анализ YAML и Python


ОБНОВЛЕНИЕ

Разбор узла вручную работал следующим образом:

name, times, zeros = None, None, None
for key, value in node.value:
    elementName = loader.construct_scalar(key)
    if elementName == 'name':
        name = loader.construct_scalar(value)
    elif elementName == 'times':
        times = loader.construct_sequence(value)
    elif elementName == 'zeros':
        zeros = loader.construct_sequence(value)
    else:
        raise ValueError('Unexpected YAML key %s' % elementName)

Но вопрос все еще остается, есть ли способ сделать это не вручную?


person gt6989b    schedule 23.03.2018    source источник
comment
Есть ли особая причина, по которой вам нужно быть совместимым с YAML 1.1 и для этого использовать PyYAML?   -  person Anthon    schedule 24.03.2018
comment
@Anthon Мне нужно использовать PyYAML, потому что это синтаксический анализатор, уже используемый системой. У вас есть другие более современные предложения? Я могу рассмотреть возможность обновления системы в какой-то момент   -  person gt6989b    schedule 25.03.2018


Ответы (2)


Вместо того

nodeMap = loader.construct_mapping(node)

попробуй это:

nodeMap = loader.construct_mapping(node, deep=True)

Кроме того, у вас есть небольшая ошибка в вашем YAML-файле:

!MyClass:

Двоеточие в конце здесь не место.

person tinita    schedule 23.03.2018
comment
потрясающе, работало как шарм. Я хочу, чтобы кто-нибудь где-нибудь четко задокументировал этот параметр deep, чтобы я не беспокоил людей здесь такими проблемами: - (... Большое вам спасибо! - person gt6989b; 23.03.2018
comment
Да, безусловно, документы можно улучшить! pyyaml.org не поддерживается текущими разработчиками pyyaml, и мы хотели бы переместить документацию в репозиторий github, и, возможно, мы также найдем время для его улучшения. - person tinita; 23.03.2018

С вашим подходом возникает множество проблем, даже если не учитывать, что вы должны прочитать PEP 8, руководство по стилю кода Python, в частности часть на Имена методов и переменные экземпляра

  1. Поскольку вы указываете, что долго просматривали документацию Python, вы не могли не заметить, что yaml.load() небезопасно. Его также почти никогда не нужно использовать, особенно если вы пишете свои собственные репрезентаторы и конструкторы.

  2. Вы используете dumper.represent_dict({data.YAMLTag : data.toDict()}), который выгружает объект как пару "ключ-значение". Что вы хотите сделать, по крайней мере, если вы хотите, чтобы в выходном YAML был тег: dumper.represent_mapping(data.YAMLTag, data.toDict()). Это даст вам вывод формы:

    !MyClass
    name: InstanceId
    times: [0.0, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0]
    zeros: [0.03, 0.03, 0.04, 0.03, 0.03, 0.02, 0.03]
    

    то есть сопоставление с тегами вместо пары "ключ-значение", где значением является сопоставление. (И я ожидал, что первая строка будет '!MyClass':, чтобы убедиться, что скаляр, начинающийся с восклицательного знака, не интерпретируется как тег).

  3. Создание сложного объекта, потенциально самореферентного (прямо или косвенно), необходимо выполнить за два шага с использованием генератора. (код PyYAML называет это правильно для вас). В вашем коде вы предполагаете, что у вас есть все параметры для создания экземпляра MyClass. Но если есть ссылка на себя, эти параметры должны включать сам этот экземпляр, и он еще не создан. Правильный пример кода в базе кода YAML для этого - construct_yaml_object() в constructor.py:

    def construct_yaml_object(self, node, cls):
        data = cls.__new__(cls)
        yield data
        if hasattr(data, '__setstate__'):
            state = self.construct_mapping(node, deep=True)
            data.__setstate__(state)
        else:
            state = self.construct_mapping(node)
            data.__dict__.update(state)
    

    Вам не обязательно использовать .__new__(), но вы должны принять во внимание deep=True, как объяснено здесь

В общем, также полезно иметь __repr__(), который позволяет вам проверять загружаемый объект с помощью чего-то более выразительного, чем <__main__.MyClass object at 0x12345>

Импорт:

from __future__ import print_function

import sys
import yaml
from cStringIO import StringIO
import numpy as np

Чтобы проверить правильность работы самореферентных версий, я добавил в класс атрибут self._ref:

class MyClass(object):
    YAMLTag = u'!MyClass'

    def __init__(self, name=None, times=[], zeros=[], ref=None):
        self.update(name, times, zeros, ref)

    def update(self, name, times, zeros, ref):
        self.name = name
        self._T = np.array(times)
        self._zeros = np.array(zeros)
        self._ref = ref

    def toDict(self):
        return dict(name=self.name,
                    times=self._T.tolist(),
                    zeros=self._zeros.tolist(),
                    ref=self._ref,
        )

    def __repr__(self):
        return "{}(name={}, times={}, zeros={})".format(
            self.__class__.__name__,
            self.name,
            self._T.tolist(),
            self._zeros.tolist(),
        )

    def update_self_ref(self, ref):
        self._ref = ref

"Методы" представителя и конструктора:

    @staticmethod
    def to_yaml(dumper, data):
        return dumper.represent_mapping(data.YAMLTag, data.toDict())

    @staticmethod
    def from_yaml(loader, node):
        value = MyClass()
        yield value
        node_map = loader.construct_mapping(node, deep=True)
        value.update(**node_map)


yaml.add_representer(MyClass, MyClass.to_yaml, Dumper=yaml.SafeDumper)
yaml.add_constructor(MyClass.YAMLTag, MyClass.from_yaml, Loader=yaml.SafeLoader)

И как им пользоваться:

instance = MyClass('InstanceId',
                   [0.0, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0],
                   [0.03, 0.03, 0.04, 0.03, 0.03, 0.02, 0.03])
instance.update_self_ref(instance)

buf = StringIO()
yaml.safe_dump(instance, buf)

yaml_str = buf.getvalue()
print(yaml_str)


data = yaml.safe_load(yaml_str)
print(data)
print(id(data), id(data._ref))

комбинация выше дает:

&id001 !MyClass
name: InstanceId
ref: *id001
times: [0.0, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0]
zeros: [0.03, 0.03, 0.04, 0.03, 0.03, 0.02, 0.03]

MyClass(name=InstanceId, times=[0.0, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0], zeros=[0.03, 0.03, 0.04, 0.03, 0.03, 0.02, 0.03]) 
139737236881744 139737236881744

Как видите, значения id для data и data._ref после загрузки одинаковы.

Вышеупомянутое вызывает ошибку, если вы используете упрощенный подход в своем конструкторе, просто используя loader.construct_mapping(node, deep=True)

person Anthon    schedule 23.03.2018
comment
Большое спасибо, это действительно помогает получить более подробный ответ с расширенными комментариями о том, как использовать различные функции. Не могли бы вы сказать мне, почему в from_yaml() вы yield перед обновлением класса? Кроме того, почему yield, а не return (я понимаю, что если вы вернетесь из середины выполнения, обновление не произойдет, но почему бы не создать, обновить, вернуть вместо create, yield, update)? - person gt6989b; 27.03.2018
comment
@ gt6989b двухэтапная ссылка в ответе содержит более подробную информацию, но сводится к рекурсивному характеру. Если объект ссылается на себя, вы можете выполнить обновление с помощью некоторых уловок. Но если a включает b и b, тогда включает c, а c включает a, вы должны передать все эти полузаполненные структуры. Выход предназначен только для размещения метода, создающего эти структуры в PyYAML, он проверяет, является ли возвращаемый объект GeneratorType, и если да, вызывает .next() для этого объекта. Это можно сделать по-другому, но, учитывая, что генераторы являются частью Python, это хороший способ сохранить - person Anthon; 28.03.2018
comment
все в одном методе - person Anthon; 28.03.2018