首页 > 解决方案 > 从 YAML 文件中加载嵌套对象的 OOP 样式的最佳实践

问题描述

我有一个描述文本游戏内容的 YAML 文件。它包含填充许多嵌套对象所需的数据。这些是场景、动作、结果和状态更新。一个场景包含许多动作,一个动作有许多可能的结果,一个结果可以导致许多状态更新。

我已经为这些实体中的每一个定义了类,但我不确定从面向对象的角度创建它们的最佳方法。我应该在一组大的嵌套 for 循环中创建所有内容(例如在游戏的 init 中)还是应该将数据传递给较低级别​​的类并逐个解析它?

使用前一种方法,我觉得对象的构造及其关系会更容易理解。但是,使用后者,我觉得编写单元测试会更容易。

我正在使用安全的 PyYAML 加载器加载它,因此将其标记为特定的 python 对象是不可行的。

请给出支持和反对每种方法或其他替代方法的理由。

标签: pythonpython-3.xoopyamlpyyaml

解决方案


尽管“标记为特定的 python 对象是不可行的”,但 使用显式标记标记它们是可行的,即使在安全加载器的限制范围内也是如此。这不会使加载 YAML 文件变得不安全,除非从这些标签创建的对象在启动时具有不安全的副作用。

使用标签还允许您转储(和加载)内存中的数据结构,即使您在开发中的某个时间点有从多个对象到另一个特定对象的引用,或者当您有具有(间接)引用的对象时对自己。这些将使用锚点和别名进行转储,并且在转储时不使用标签自行解决这些问题并非易事,因为您需要跟踪已转储的对象。

个人的单元测试可以很容易地编写。通常你唯一需要确保的是你可以创建一个没有动作的场景,然后你可以测试不涉及动作的场景的所有内容,等等。

PyYAML 可以解析所有 YAML 1.1 规范,但无法加载所有该规范。此外,YAML 1.2 已经推出十年了。两者都不是您的程序的限制,但我建议使用ruamel.yaml没有这些问题的 (免责声明:我是该软件包的作者)。此处ruamel.yaml描述了如何转储/加载类

import sys
from pathlib import Path
import ruamel.yaml

data_file = Path('data.yaml')

yaml = ruamel.yaml.YAML(typ='safe')

@yaml.register_class
class Scene:
    def __init__(self, sc_parm, actions):
       self.sc_parm = sc_parm
       self.actions = actions

@yaml.register_class
class Action:
    def __init__(self, ac_parm1, ac_parm2):
        self.ac_parm1 = ac_parm2
        self.ac_parm2 = ac_parm2


shared_action = Action('south', 1)

data = [
    Scene('sc1_parm_val', []), 
    Scene('sc2_parm_val', [Action('north', 3), shared_action]),
    Scene('sc3_parm_val', [shared_action, Action('west', 2)]),
]

yaml.dump(data, data_file)

print(data_file.read_text(), end='')
print('=+'*30)
d2 = yaml.load(data_file)

for d in d2:
    print(d.sc_parm, d.actions)

这使:

- !Scene
  actions: []
  sc_parm: sc1_parm_val
- !Scene
  actions:
  - !Action {ac_parm1: 3, ac_parm2: 3}
  - &id001 !Action {ac_parm1: 1, ac_parm2: 1}
  sc_parm: sc2_parm_val
- !Scene
  actions:
  - *id001
  - !Action {ac_parm1: 2, ac_parm2: 2}
  sc_parm: sc3_parm_val
=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
sc1_parm_val []
sc2_parm_val [<__main__.Action object at 0x7f009be56f28>, <__main__.Action object at 0x7f009be56fd0>]
sc3_parm_val [<__main__.Action object at 0x7f009be56fd0>, <__main__.Action object at 0x7f009be56e48>]

Action对象的 ID 可以看出,第二个场景的第二个动作再次与最后一个场景的第一个动作相同(原始shared_action

确保,如果您决定自己制作from_yaml(而不是依赖通过注册获得的默认值),例如为了限制被转储的属性,您使用创建对象的两步过程,因为这是必要的允许树中的递归数据结构。您可能不需要立即使用它,但它很容易做到,例如 这里描述的往返加载器(加载然后转储 YAML 时可以保留注释、引用样式、别名等的安全加载器)。


推荐阅读