You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用ruamel.yaml处理自定义类时保留注释与格式的问题

ruamel.yaml 0.17.21:自定义类往返时保留注释与格式的问题解决

问题

使用ruamel.yaml 0.17.21实现YAML和自定义类的加载、修改、转储,要求全程保留注释和原格式,但往返后出现:

  • 部分注释丢失
  • 行内注释被移至单独行
  • 空白行(ruamel.yaml视为注释)消失

尝试用a['entry'].__init__(a['entry'].__dict__)初始化类,还是丢失多数注释和空白行。空白行可接受先清除再在顶级条目间重新插入的方案,不确定是操作错误还是需要提bug。

最小复现示例

输入YAML(test.yaml)

# 顶级注释
entry:
  # 条目注释
  key1: value1  # 行内注释
  key2: value2

# 另一个顶级注释
another_entry:
  foo: bar

测试代码

import ruamel.yaml
from dataclasses import dataclass

@dataclass
class Entry:
    key1: str
    key2: str

yaml = ruamel.yaml.YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.preserve_quotes = True

# 加载
with open('test.yaml', 'r') as f:
    data = yaml.load(f)

# 转成自定义类
data['entry'] = Entry(**data['entry'])

# 修改
data['entry'].key1 = 'new_value1'

# 转储
with open('output.yaml', 'w') as f:
    yaml.dump(data, f)

异常输出(output.yaml)

# 顶级注释
entry:
  key1: new_value1
  key2: value2
# 条目注释
# 行内注释

another_entry:
  foo: bar
# 另一个顶级注释

问题根源

ruamel.yaml的注释、行内注释、空白行等格式信息,都是绑定在它内部的CommentedMap、CommentedSeq这些特殊结构上的。直接把这些结构转换成自定义类实例,相当于丢弃了所有附着的格式元数据,自然会出现注释丢失、格式错乱的问题。

解决方案

方案1:注册自定义类,保留注释节点

通过yaml.register_class给自定义类添加YAML序列化/反序列化逻辑,在加载时保存原始注释节点,转储时复用该节点来保留格式:

import ruamel.yaml
from ruamel.yaml.nodes import MappingNode

class Entry:
    def __init__(self, key1=None, key2=None):
        self.key1 = key1
        self.key2 = key2

    @classmethod
    def from_yaml(cls, constructor, node):
        # 加载节点并保存原始映射(含注释)
        mapping = constructor.construct_mapping(node, deep=True)
        instance = cls(**mapping)
        instance._yaml_node = node  # 保存原始节点用于转储
        return instance

    @classmethod
    def to_yaml(cls, representer, data):
        # 用原始节点生成YAML,保留所有注释格式
        return representer.represent_mapping('tag:yaml.org,2002:map', data._yaml_node)

yaml = ruamel.yaml.YAML()
yaml.register_class(Entry)

# 加载、修改、转储流程不变,但注释会被保留
with open('test.yaml', 'r') as f:
    data = yaml.load(f)

data['entry'].key1 = 'new_value1'

with open('output.yaml', 'w') as f:
    yaml.dump(data, f)

方案2:用包装类代理CommentedMap

如果不想修改类的序列化逻辑,可以用包装类包裹CommentedMap,既保留注释元数据,又能通过类属性访问和修改值:

import ruamel.yaml
from ruamel.yaml.comments import CommentedMap

class Entry:
    def __init__(self, commented_map):
        self._map = commented_map  # 保留原始CommentedMap

    @property
    def key1(self):
        return self._map['key1']

    @key1.setter
    def key1(self, value):
        self._map['key1'] = value

    @property
    def key2(self):
        return self._map['key2']

    @key2.setter
    def key2(self, value):
        self._map['key2'] = value

yaml = ruamel.yaml.YAML()
with open('test.yaml', 'r') as f:
    data = yaml.load(f)

# 用原始CommentedMap初始化包装类
entry = Entry(data['entry'])
entry.key1 = 'new_value1'

# 直接转储原始data对象,注释仍在CommentedMap中
with open('output.yaml', 'w') as f:
    yaml.dump(data, f)

空白行手动修复方案

如果需要统一在顶级条目间添加空白行,可以在转储前遍历顶级CommentedMap,手动插入空注释实现空白行:

# 假设data是顶级CommentedMap
top_keys = list(data.keys())
for idx in range(1, len(top_keys)):
    current_key = top_keys[idx]
    # 在当前键前添加空白行
    data.yaml_set_comment_before_key(current_key, '\n')

yaml.dump(data, open('output.yaml', 'w'))

总结

这不是bug,是操作逻辑问题——自定义类和ruamel.yaml的注释元数据不兼容导致的。用上述两种方案就能实现保留注释和格式的类往返操作。如果遇到极端场景的格式丢失,可以去ruamel.yaml的官方仓库确认是否为已知问题。

内容的提问来源于stack exchange,提问作者JeanOlivier

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 15:25:17