Пользовательский тег PyYAML для кортежа кортежей

Я планирую использовать PyYAML для файла конфигурации. Некоторые элементы в этом файле конфигурации представляют собой кортежи кортежей Python. Итак, мне нужен удобный способ их представления. Кортежи кортежей Python можно представить следующим образом, используя PyYAML

print yaml.load("!!python/tuple [ !!python/tuple [1, 2], !!python/tuple [3, 4]]")

Однако это неудобное обозначение для длинной последовательности элементов. Я думаю, что должна быть возможность определить собственный тег, например python / tuple_of_tuples. Т.е. что-то типа

yaml.load("!!python/tuple_of_tuples [[1,2], [3,4]]")

См. Мою первую попытку определить это ниже, имитируя определение python / tuple и пытаясь создать аналогичные подклассы. Это не удается, но, я думаю, дает представление о том, что мне нужно. У меня есть вторая попытка, которая работает, но это чит, поскольку он просто вызывает eval.

Если я не найду ничего лучше, я просто воспользуюсь этим. Однако YAML предназначен для замены ConfigObj, который использует файлы INI и значительно менее мощный, чем YAML, и я использовал тот же подход (а именно eval) для кортежей кортежей. Так что в этом отношении не будет хуже.

Было бы желательно найти правильное решение.

У меня есть пара комментариев по поводу моего первого решения.

  1. Я бы подумал, что конструктор construct_python_tuple_of_tuples вернет завершенную структуру, но на самом деле он, кажется, возвращает пустую структуру следующим образом

    ([], [])
    

    Я отследил вызовы, и, похоже, после вызова construct_python_tuple_of_tuples происходит много сложных вещей.

    Возвращаемое значение представляет собой набор списков целых чисел, что очень близко к желаемому результату. Значит, конструкция должна быть завершена позже.

    Линия с

    tuple([tuple(t) for t in x])
    

    была моей попыткой привести список кортежей к кортежу кортежей, но если я верну его из construct_python_tuple_of_tuples, то полученный вызов yaml.load("!!python/tuple_of_tuples [[1,2], [3,4]]") будет просто

    ((),())
    
  2. Не уверен, что с

    yaml.org,2002
    

    Почему 2002 год?

Первая попытка

import yaml
from yaml.constructor import Constructor

def construct_python_tuple_of_tuples(self, node):
     # Complete content of construct_python_tuple
     # is
     # return tuple(self.construct_sequence(node))

     print "node", node
     x = tuple(self.construct_sequence(node))
     print "x", x
     foo = tuple([tuple(t) for t in x])
     print "foo", foo
     return x

Constructor.construct_python_tuple_of_tuples =
construct_python_tuple_of_tuples

Constructor.add_constructor(
         u'tag:yaml.org,2002:python/tuple_of_tuples',
         Constructor.construct_python_tuple_of_tuples)

y = yaml.load("!!python/tuple_of_tuples [[1,2], [3,4]]")
print "y", y, type(y)
print y[0], type(y[0])
print y[0][0], type(y[0][0])

Результаты

node SequenceNode(tag=u'tag:yaml.org,2002:python/tuple_of_tuples',
value=[SequenceNode(tag=u'tag:yaml.org,2002:seq',
value=[ScalarNode(tag=u'tag:yaml.org,2002:int', value=u'1'),
ScalarNode(tag=u'tag:yaml.org,2002:int', value=u'2')]),
SequenceNode(tag=u'tag:yaml.org,2002:seq',
value=[ScalarNode(tag=u'tag:yaml.org,2002:int', value=u'3'),
ScalarNode(tag=u'tag:yaml.org,2002:int', value=u'4')])])

x ([], [])

foo ((), ())

y ([1, 2], [3, 4]) <type 'tuple'>

y[0] [1, 2] <type 'list'>

y[0][0] 1 <type 'int'>

Вторая попытка

import yaml
from yaml import YAMLObject, Loader, Dumper

class TupleOfTuples(YAMLObject):
    yaml_loader = Loader
    yaml_dumper = Dumper

    yaml_tag = u'!TupleOfTuples'
    #yaml_flow_style = ...

    @classmethod
    def from_yaml(cls, loader, node):
        import ast
        print "node", node
    print "node.value", node.value, type(node.value)
        return ast.literal_eval(node.value)

    @classmethod
    def to_yaml(cls, dumper, data):
        return node

t = yaml.load("!TupleOfTuples ((1, 2), (3, 4))")
print "t", t, type(t)

Результаты следующие:

node ScalarNode(tag=u'!TupleOfTuples', value=u'((1, 2), (3, 4))')
node.value ((1, 2), (3, 4)) <type 'unicode'>
t ((1, 2), (3, 4)) <type 'tuple'>

person Faheem Mitha    schedule 24.01.2013    source источник
comment
Я полагаю, что просто загрузка списков, как обычно, а затем преобразование списков в кортежи перед передачей конфигурации остальной части кода, неприемлемо?   -  person    schedule 24.01.2013
comment
@delnan Списки списков целых чисел, вероятно, подойдут. Разве это легче сделать?   -  person Faheem Mitha    schedule 24.01.2013
comment
Это тривиально, поскольку YAML изначально поддерживает списки и целые числа ;-)   -  person    schedule 24.01.2013
comment
@delnan Так оно и есть. Интересно, почему он также не может обрабатывать кортежи кортежей. Может быть, я смогу справиться со списками списков. Я попытаюсь.   -  person Faheem Mitha    schedule 25.01.2013


Ответы (1)


Начнем с вопроса 2: 2002 год был годом, когда этот вид тега был введен в версию черновик YAML 1.0

Вопрос 1 сложнее. Если вы это сделаете:

from __future__ import print_function

import yaml

lol = [[1,2], [3,4]]  # list of lists
print(yaml.dump(lol))

вы получите (A):

[[1, 2], [3, 4]]

Но на самом деле это сокращение от (B):

!!seq [
  !!seq [
    !!int "1",
    !!int "2",
  ],
  !!seq [
    !!int "3",
    !!int "4",
  ],
]

что является сокращением от (C):

!<tag:yaml.org,2002:seq> [
  !<tag:yaml.org,2002:seq> [
    !<tag:yaml.org,2002:int> "1",
    !<tag:yaml.org,2002:int> "2",
  ],
  !<tag:yaml.org,2002:seq> [
    !<tag:yaml.org,2002:int> "3",
    !<tag:yaml.org,2002:int> "4",
  ],
]

Все A, B и C загружаются в исходный список list, потому что seq (uence) является встроенным типом.

Я не думаю, что расширение синтаксиса yaml (например, (), указывающее кортеж, было бы хорошей идеей. Чтобы минимизировать теги, вы уменьшите свой пример до:

yaml_in = "!tuple [ !tuple [1, 2], !tuple [3, 4]]"

и добавляем конструктор:

yaml.add_constructor("!tuple", construct_tuple)

но это подталкивает к созданию функции construct_tuple. Один для последовательности (в constructor.py):

def construct_yaml_seq(self, node):
    data = []
    yield data
    data.extend(self.construct_sequence(node))

Но вы не можете просто заменить там [] на (), поскольку изменение кортежа путем его расширения не сработает (причина этого двухэтапного создания с yield, например, в том, чтобы разрешить циклические ссылки в сложных типах, таких как последовательность и сопоставление).

Вы должны определить класс Tuple(), который ведет себя как список до тех пор, пока он не будет «заблокирован» (что вы сделали бы в конце конструкции), и с этого момента он должен вести себя как кортеж (т.е. без каких-либо модификаций). Следующее делает это без подкласса yaml.YAMLObject, поэтому вы должны явно предоставить и зарегистрировать конструктор и представитель класса.

class Tuple(list):

    def _lock(self):
        if hasattr(self, '_is_locked'):
            return
        self._is_locked = True
        self.append = self._append
        self.extend = self._extend

    def _append(self, item):
        raise AttributeError("'Tuple' object has no attribute 'append'")

    def _extend(self, items):
        raise AttributeError("'Tuple' object has no attribute 'extend'")

    def __str__(self):
        return '(' + ', '.join((str(e) for e in self)) + ')'

    # new style class cannot assign something to special method
    def __setitem__(self, key, value):
        if getattr(self, '_is_locked', False):
            raise TypeError("'Tuple' object does not support item assignment")
        list.__setitem__(self, key, value)

    def __delitem__(self, key, value):
        if getattr(self, '_is_locked', False):
            raise TypeError("'Tuple' object does not support item deletion")
        list.__delitem__(self, key, value)

    @staticmethod
    def _construct_tuple(loader, data):
        result = Tuple()
        yield result
        result.extend(loader.construct_sequence(data))
        result._lock()

    @staticmethod
    def _represent_tuple(dumper, node):
        return dumper.represent_sequence("!tuple", node)

# let yaml know how to handle this
yaml.add_constructor("!tuple", Tuple._construct_tuple)
yaml.add_representer(Tuple, Tuple._represent_tuple)

Имея это на месте, вы можете:

yaml_in = "!tuple [ !tuple [1, 2], !tuple [3, 4]]"
#yaml_in = "!tuple [1, 2]"

data = yaml.load(yaml_in)
print(data)
print(data[1][0])
print(type(data))

получить:

((1, 2), (3, 4))
3
<class '__main__.Tuple'>

Это не настоящий tuple, но он не допускает действий, подобных list. Все следующие действия вызывают соответствующую ошибку:

# test appending to the tuple,
try:
    data.append(Tuple([5, 6]))
except AttributeError:
    pass
else:
    raise NotImplementedError
# test extending the tuple,
try:
    data.extend([5, 6])
except AttributeError:
    pass
else:
    raise NotImplementedError
# test replacement of an item
try:
    data[0] = Tuple([5, 6])
except TypeError:
    pass
else:
    raise NotImplementedError
# test deletion of an item
try:
    del data[0]
except TypeError:
    pass
else:
    raise NotImplementedError

И, наконец, вы можете:

print(yaml.dump(data, default_flow_style=True))

для следующего вывода:

!tuple [!tuple [1, 2], !tuple [3, 4]]

Если вы действительно хотите !tuple [[1, 2], [3, 4]] создать кортеж кортежей, вы можете сделать это, сохранив состояние контекста в классе Baseloader yaml и переопределив метод, создающий объект python из последовательностей в кортежи или списки в зависимости от контекста. Вероятно, это должен быть стек состояний контекста, чтобы разрешить вложенное использование! Tuple, а также не-вложенное использование и некоторое явное переопределение для получения списков внутри кортежей при использовании !!seq в качестве тега.


Возможно, я не проверял Tuple() на полноту, а реализовал только те ограничения, которые tuple по сравнению с list, которые сразу пришли в голову.
Я тестировал это с моей расширенной версией PyYAML: ruamel.yaml, но это должно работать точно так же в самом PyYAML.

person Anthon    schedule 14.04.2015