【问题标题】:Custom dumper in pyYAMLpyYAML 中的自定义转储程序
【发布时间】:2019-08-11 19:55:04
【问题描述】:

我想自定义将字典转储到 yaml 文件的方式。 更准确地说,给定以下字典:

{'a': 1,
 'b': 2,
 '__duplicate__c': [3, 4, 5]}

我想将它转储到一个 yaml 文件中,其中以 __duplicate__ 开头的键处理如下:

'a': 1
'b': 2
'c': 3
'c': 4
'c': 5

我已经编写了一个自定义构造函数子类yaml.constructor.BaseConstructor 和一个自定义加载器,它可以读取带有重复键的 YAML 文件,我现在希望也可以编写带有重复键的 yaml 文件。

【问题讨论】:

  • 您显示的输出不是有效的 YAML(YAML 禁止重复键)。由于您正在编写不是 YAML 文件的内容,因此 YAML 库可能对您没有帮助。
  • 话虽如此,就像我在问题中提到的那样,我能够子类化一个加载器来加载这些带有重复键的 invalid YAML 文件。为什么不反其道而行之?
  • 因为您基本上依赖于未定义的行为,这些行为可能会在库的任何更新中消失。这要求维护地狱。
  • @BiBi 主要问题在于您的代表。您不应该为格式化的键分配特殊含义,从而掩盖它们的正常使用。而是创建一个像 dict 一样工作的类,它使用一组跟踪特殊键,并且具有查询键是否具有“重复”含义的特殊方法。然后,您可以通过为该类创建一个普通表示器来编写您的(无效)YAML。加载需要通过继承 BaseConstructor 来完成,否则 PyYAML 已经吞噬了 c 的前面值(它应该给出警告并保留找到的第一个值)。
  • @Anthon 你的方法是我想要实现的。我已经将 BaseConstructor 子类化以在加载(无效)YAML 时处理重复键——它不会丢弃重复键,而是在某些结构中跟踪它们。话虽如此,只要我有子类 dict 来处理重复键,我不知道如何将这个特殊 dict 转储到 YAML 文件中以获得预期的输出(具有重复键的无效 YAML)。

标签: python yaml pyyaml


【解决方案1】:

有一些事情并不完全清楚,但假设这不是 只是一个编程练习,我认为你有从某个地方读入的数据 有值对,并且你:

  • 希望根据这些对的第一个值进行查找
  • 第一个值可以有重复项
  • 您需要能够将其转储到 YAML 并从 YAML 加载回来

在下面我假设你的输入数据看起来像下面的 Python,但它可以 当然也来自一些数据文件,或者一些其他可以处理重复的结构(Python dicts 不能):

data = [('a', 1),
        ('b', 2),
        ('c', 3),
        ('c', 4),
        ('c', 5),
]

Python 中的表示

您表示您将数据存储在具有重复键的普通字典中 转换为__duplicate__c,一般来说这不是一个好的选择 因为它会导致问题。

首先,您不能将方法添加到处理这些特殊的内置字典 键,因此您的代码将在您的程序和/或对这些的测试中浮动 特殊键重复。

其次,这使得表格的原始数据条目:

      ('__duplicate__X': 42),

没有额外的处理就不可能存储,就像条目一样

      (4, 1),
      ('4', 2),
      (4, 3),
      ('4', 4),

,因为在一般情况下,您必须决定如何处理 非字符串重复值,如果将它们映射到字符串,则会屏蔽字符串版本 那些“钥匙”。

即使您的数据仅限于第一个数据项对 作为一个字符串,最好创建一个dict 类,或者 子类dict,这样你就可以在内部保留一个列表 重复的键并以特殊方式处理它们 cq.需要特别的 访问方法。

YAML 中的表示

您建议转储的方式会导致 YAML 无效。没有其他程序可以简单地加载这些数据。 在现代 YAML (1.2) 中,映射中的键必须是唯一的,如果不是这种情况是错误的。 在现已过时十年的 YAML 1.1 规范中,其中 PyYAML 是部分实现, 您可以忽略第二个键并发出适当的警告。请注意

使用标签转储数据更为合适,例如:

!MultiDict
- [c]
- a: 1
  b: 2
  c: [3, 4, 5]

上面明确需要该标签的处理程序,并且加载将 如果不处理则失败。以您的“YAML”表示形式作为输入 未修改的 PyYAML 既不给出警告错误也不加载正确的 值 (3)。

(PyPI上有一个multidict包,但是不能处理非字符串键。)

import sys
from ruamel.yaml import YAML, yaml_object
from ruamel.yaml.compat import StringIO

yaml = YAML()
yaml.default_flow_style = None

data = [('a', 1),
        ('b', 2),
        ('c', 3),
        ('c', 4),
        ('c', 5),
        (4, 1),
        ('4', 2),
        (4, 3),
        ('4', 4),
]

class MultiDictDuplicateKeyError(Exception):
    pass

@yaml_object(yaml)
class MultiDict(dict):
    """ no support for deletion of keys """
    yaml_tag = '!MultiDict'

    def __init__(self, *args, **kw):
        self.__duplicate__ = []
        if args and not isinstance(args[0], dict):
            dict.__init__(self, **kw)
            for arg in args[0]:
                self[arg[0]] = arg[1]
        else:
            dict.__init__(self, *args, **kw)

    def __setitem__(self, key, value):
        if key in self:
            if key in self.__duplicate__:  # it should already be a list
                dict.__getitem__(self, key).append(value)
            else:
                self.__duplicate__.append(key)
                dict.__setitem__(self, key, [dict.__getitem__(self, key), value])
        else:
             dict.__setitem__(self, key, value)

    def __getitem__(self, key):
        if key in self.__duplicate__:
            raise MultiDictDuplicateKeyError("should use multival(key)")
        return dict.__getitem__(self, key)

    def multival(self, key):
        assert key in self.__duplicate__
        values = dict.__getitem__(self, key)
        assert isinstance(values, list)
        for value in values:
             yield value

    def __str__(self):
        return repr({('__duplicate__' + repr(k) if k in self.__duplicate__ else k): v 
                    for k, v in self.items()})

    @classmethod
    def to_yaml(cls, representer, node):
        return representer.represent_sequence(cls.yaml_tag, [node.__duplicate__, dict(node)])

    @classmethod
    def from_yaml(cls, constructor, node):
        # necessary to deal with recursive data structures
        for y in constructor.construct_yaml_map(node.value[1]):
            pass
        for dup in constructor.construct_yaml_seq(node.value[0]):
            pass
        ret = cls(y)
        ret.__duplicate__ = dup
        return ret

md = MultiDict(data)

print('md', md)
print('='*40)
for k in md:
    try:
        print(repr(k), '->', md[k])
    except MultiDictDuplicateKeyError:
        for v in md.multival(k):
            print(repr(k), '->', v)
print('='*40)
yaml.dump(md, sys.stdout)           
print('='*40)

buf = StringIO()
yaml.dump(md, buf)
rt_data = yaml.load(buf.getvalue())
print('rt:', rt_data)

给出:

md {'a': 1, 'b': 2, "__duplicate__'c'": [3, 4, 5], '__duplicate__4': [1, 3], "__duplicate__'4'": [2, 4]}
========================================
'a' -> 1
'b' -> 2
'c' -> 3
'c' -> 4
'c' -> 5
4 -> 1
4 -> 3
'4' -> 2
'4' -> 4
========================================
!MultiDict
- [c, 4, '4']
- a: 1
  b: 2
  c: [3, 4, 5]
  4: [1, 3]
  '4': [2, 4]
========================================
rt: {'a': 1, 'b': 2, "__duplicate__'c'": [3, 4, 5], '__duplicate__4': [1, 3], "__duplicate__'4'": [2, 4]}

我在这里使用ruamel.yaml(免责声明我是该软件包的作者),但是 进行一些调整(使用add_representeradd_constructor)得到相同的结果 应该可以通过使用 PyYAML 来实现,而不需要继承 BaseConstructor

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-05-12
    • 1970-01-01
    • 1970-01-01
    • 2014-01-15
    • 2021-05-20
    • 2015-05-10
    • 2017-08-20
    相关资源
    最近更新 更多