使用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
相关产品推荐
相关产品推荐

