我正在使用YAML配置文件。这是在Python中加载我的配置的代码:
import os import yaml with open('./config.yml') as file: config = yaml.safe_load(file)
这段代码实际上创建了一个字典。现在的问题是,为了访问值,我需要使用大量的括号。
YAML:
mysql: user: pass: secret
蟒蛇:
import os import yaml with open('./config.yml') as file: config = yaml.safe_load(file) print(config['mysql']['user']['pass']) # <--
我更喜欢这样的东西(点符号):
config('mysql.user.pass')
因此,我的想法是利用PyStache render()接口。
import os import yaml with open('./config.yml') as file: config = yaml.safe_load(file) import pystache def get_config_value( yml_path, config ): return pystache.render('{{' + yml_path + '}}', config) get_config_value('mysql.user.pass', config)
那将是一个“好的”解决方案吗?如果没有,什么是更好的选择?
附加问题[已解决]
我决定使用IljaEverilä的解决方案。但是现在我还有一个问题:如何围绕DotConf创建包装器Config类?
以下代码不起作用,但希望您能理解我要执行的操作:
class Config( DotDict ): def __init__( self ): with open('./config.yml') as file: DotDict.__init__(yaml.safe_load(file)) config = Config() print(config.django.admin.user)
错误:
AttributeError: 'super' object has no attribute '__getattr__'
解
您只需要传递self给超类的构造函数即可。
self
DotDict.__init__(self, yaml.safe_load(file))
更好的解决方法(IljaEverilä)
super().__init__(yaml.safe_load(file))
您可以用来reduce从配置中提取值:
reduce
In [41]: config = {'asdf': {'asdf': {'qwer': 1}}} In [42]: from functools import reduce ...: ...: def get_config_value(key, cfg): ...: return reduce(lambda c, k: c[k], key.split('.'), cfg) ...: In [43]: get_config_value('asdf.asdf.qwer', config) Out[43]: 1
如果您的YAML使用的语言子集非常有限,则此解决方案易于维护并且几乎没有新的边缘情况。
使用适当的YAML解析器和工具,例如此答案:
一方面,您的示例通过使用正确的方法,get_config_value('mysql.user.pass', config)而不是通过属性来解决点分访问。我不确定您是否意识到您是故意不做更直观的事情:
get_config_value('mysql.user.pass', config)
print(config.mysql.user.pass)
即使是重载__getattr__,passPython语言元素也无法正常工作。
__getattr__
但是,您的示例仅描述了非常有限的YAML文件子集,因为它不涉及任何序列集合,也不涉及任何复杂的键。
如果您想覆盖的范围不只微小的子集,可以例如扩展以下功能强大的往返对象ruamel.yaml:¹
import ruamel.yaml def mapping_string_access(self, s, delimiter=None, key_delim=None): def p(v): try: v = int(v) except: pass return v # possible extend for primitives like float, datetime, booleans, etc. if delimiter is None: delimiter = '.' if key_delim is None: key_delim = ',' try: key, rest = s.split(delimiter, 1) except ValueError: key, rest = s, None if key_delim in key: key = tuple((p(key) for key in key.split(key_delim))) else: key = p(key) if rest is None: return self[key] return self[key].string_access(rest, delimiter, key_delim) ruamel.yaml.comments.CommentedMap.string_access = mapping_string_access def sequence_string_access(self, s, delimiter=None, key_delim=None): if delimiter is None: delimiter = '.' try: key, rest = s.split(delimiter, 1) except ValueError: key, rest = s, None key = int(key) if rest is None: return self[key] return self[key].string_access(rest, delimiter, key_delim) ruamel.yaml.comments.CommentedSeq.string_access = sequence_string_access
设置完成后,您可以运行以下命令:
yaml_str = """\ mysql: user: pass: secret list: [a: 1, b: 2, c: 3] [2016, 9, 14]: some date 42: some answer """ yaml = ruamel.yaml.YAML() config = yaml.load(yaml_str) def get_config_value(path, data, **kw): return data.string_access(path, **kw) print(get_config_value('mysql.user.pass', config)) print(get_config_value('mysql:user:pass', config, delimiter=":")) print(get_config_value('mysql.list.1.b', config)) print(get_config_value('mysql.2016,9,14', config)) print(config.string_access('mysql.42'))
给予:
secret secret 2 some date some answer
这表明您只需花更多的精力和很少的额外工作,就可以灵活地对许多YAML文件进行点扩展访问,而不仅仅是那些由以字符串标量为键的递归映射组成的文件。
config.string_access(mysql.user.pass)
get_config_value()
简要说明一下(不要太在意),您可以创建一个包装器,以允许使用属性访问:
In [47]: class DotConfig: ...: ...: def __init__(self, cfg): ...: self._cfg = cfg ...: def __getattr__(self, k): ...: v = self._cfg[k] ...: if isinstance(v, dict): ...: return DotConfig(v) ...: return v ...: In [48]: DotConfig(config).asdf.asdf.qwer Out[48]: 1
请注意,这对于诸如“ as”,“ pass”,“ if”之类的关键字失败。
最后,您可能会变得非常疯狂(阅读:可能不是一个好主意),并进行自定义dict以处理点缀字符串和元组键(作为特殊情况),并可以对混合中抛出的项进行属性访问(有其局限性):
dict
In [58]: class DotDict(dict): ...: ...: # update, __setitem__ etc. omitted, but required if ...: # one tries to set items using dot notation. Essentially ...: # this is a read-only view. ...: ...: def __getattr__(self, k): ...: try: ...: v = self[k] ...: except KeyError: ...: return super().__getattr__(k) ...: if isinstance(v, dict): ...: return DotDict(v) ...: return v ...: ...: def __getitem__(self, k): ...: if isinstance(k, str) and '.' in k: ...: k = k.split('.') ...: if isinstance(k, (list, tuple)): ...: return reduce(lambda d, kk: d[kk], k, self) ...: return super().__getitem__(k) ...: ...: def get(self, k, default=None): ...: if isinstance(k, str) and '.' in k: ...: try: ...: return self[k] ...: except KeyError: ...: return default ...: return super().get(k, default=default) ...: In [59]: dotconf = DotDict(config) In [60]: dotconf['asdf.asdf.qwer'] Out[60]: 1 In [61]: dotconf['asdf', 'asdf', 'qwer'] Out[61]: 1 In [62]: dotconf.asdf.asdf.qwer Out[62]: 1 In [63]: dotconf.get('asdf.asdf.qwer') Out[63]: 1 In [64]: dotconf.get('asdf.asdf.asdf') In [65]: dotconf.get('asdf.asdf.asdf', 'Nope') Out[65]: 'Nope'