小编典典

Python:使用“点符号”访问YAML值

python

我正在使用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给超类的构造函数即可。

DotDict.__init__(self, yaml.safe_load(file))

更好的解决方法(IljaEverilä)

super().__init__(yaml.safe_load(file))

阅读 216

收藏
2020-12-20

共1个答案

小编典典

简单

您可以用来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)而不是通过属性来解决点分访问。我不确定您是否意识到您是故意不做更直观的事情:

print(config.mysql.user.pass)

即使是重载__getattr__,passPython语言元素也无法正常工作。

但是,您的示例仅描述了非常有限的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文件进行点扩展访问,而不仅仅是那些由以字符串标量为键的递归映射组成的文件。

  1. 如图所示,您可以直接调用config.string_access(mysql.user.pass)而不是定义和使用get_config_value()
  2. 这适用于字符串和整数作为映射键,但可以轻松扩展以支持其他键类型(布尔,日期,日期时间)。

令人费解的

简要说明一下(不要太在意),您可以创建一个包装器,以允许使用属性访问:

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以处理点缀字符串和元组键(作为特殊情况),并可以对混合中抛出的项进行属性访问(有其局限性):

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'
2020-12-20