基于 Python 的配置文件至少在 Python 1.6 中以 distutils'setup.py 的形式存在(即 2000 年之前)。使用这种格式的主要缺点是很难以编程方式更新配置中的值。即使您只想制作一些额外的实用程序来分析这些文件,您甚至必须特别注意您可以在不执行代码的情况下导入这样的配置文件,而且也不需要通过导入来拉入各种依赖项。这可以通过使用if __name__ == '__main__': 来实现,或者更容易通过将配置信息作为文件中的数据结构来实现。
因此,如果更新文件永远不会成为问题,那么您可以使用基于 Python 的数据结构,并且这些数据结构非常易读。
XML 和 JSON 不是手动编辑的好格式。 XML 有很多 < 和 > 可以在没有特殊工具的情况下轻松输入。 JSON 有很多双引号,难以阅读,但它也存在各种问题,因为 JSON 不允许在数组和对象中使用尾随逗号,从而导致人们编写如下对象:
{
"a": 1
, "b": 2
}
这可以防止您删除最后一行并忘记删除分隔键/值对的逗号,但 IMO 可读性不同。
另一方面,YAML 可以写得很可读,但是在编辑文件时必须考虑一些规则。在我的回答here 中,我展示了一些可以包含在 YAML 文件中的基本规则,编辑者在编辑时需要考虑这些规则。 YAML 可以被 Python 以外的其他语言读取(使用基于 Python 的配置文件很难做到这一点)。
您可以使用 YAML 标签(以及与这些标签关联的适当 Python 对象),因此您不必依赖从某个键值对中解释键来理解值的含义:
- !Job
calendar: !Calendar indian
dependency: !Arbitrary_python_code checkIfTuesdayAndNotHoliday()
command: !CommandTester
exec: !Exec check availability of flight
success: !Commands
- !Notify
email: agrawall
- !Exec some command to book my flight
failure: !Commands
- !Notify
email: ops
(底部是与这些标签关联的类的部分示例实现)
当您使用 ruamel.yaml(免责声明:我是该软件包的作者)时,即使不丢失 cmets、密钥排序、标签,也可以通过编程方式更新 YAML。
我一直在参数化我的 Python 打包(我管理超过 100 个包,其中一些在 PyPI 上,其他仅用于特定客户端),通过从每个包的__init__.py 文件。我已经尝试插入 Python 的 JSON 子集,但最终开发了 PON(Python 对象表示法),它可以很容易地被 setup.py 解析,而无需在 AST 上导入带有小(100 行)扩展名的 __init__.py 文件literal_eval 包含在 Python 标准库中。
PON 可以在没有任何库的情况下使用(因为它是 Python 数据结构的子集,包括 dict、list、set、tuple 和基本类型,如整数、浮点数、布尔值、字符串、日期、日期时间。因为它基于AST 评估器,您可以在配置文件中进行计算 (secs_per_day = 24 * 60 * 60) 和其他评估。
PON 自述文件还更详细地描述了该格式相对于 YAML、JSON、INI、XML 的优点(和缺点)。
使用配置数据不需要 PON 包,仅当您想对 PON 数据进行编程往返(加载-编辑-转储)时才需要。
import sys
from ruamel.yaml import YAML, yaml_object
yaml = YAML()
@yaml_object(yaml)
class CommandTester:
yaml_tag = u'!CommandTester'
def __init__(self, exec=None, success=None, failure=None):
self.exec = exec
self.success = success
self.failure = failure
def __call__(self):
if self.exec():
self.success()
else:
self.failure()
@yaml_object(yaml)
class Commands:
"""a list of commands"""
yaml_tag = u'!Commands'
def __init__(self, commands):
self._commands = commands # list of commands to execute
@classmethod
def from_yaml(cls, constructor, node):
for m in yaml.constructor.construct_yaml_seq(node):
pass
return cls(m)
@classmethod
def to_yaml(cls, representer, node):
return representer.represent_sequence(cls.yaml_tag, node._commands)
def __call__(self, verbose=0, stop_on_error=False):
res = True
for cmd in self._cmd:
try:
res = subprocess.check_output(cmd)
except Exception as e:
res = False
if stop_on_error:
break
return res
@yaml_object(yaml)
class Command(Commands):
"""a single command"""
yaml_tag = u'!Exec'
def __init__(self, command):
Commands.__init__(self, [command])
@classmethod
def from_yaml(cls, constructor, node):
return cls(node.value)
@classmethod
def to_yaml(cls, representer, node):
return representer.represent_scalar(cls.yaml_tag, node._commands[0])
@yaml_object(yaml)
class Notifier:
yaml_tag = u'!Notify'
with open("job.yaml") as fp:
job = yaml.load(fp)
yaml.dump(job, sys.stdout)