Python: 使用“点表示法”访问YAML值

13

我正在使用YAML配置文件。下面是在Python中加载我的配置文件的代码:

import os
import yaml
with open('./config.yml') as file:
    config = yaml.safe_load(file)

这段代码实际上创建了一个字典。现在的问题是,为了访问值,我需要使用大量的方括号。

YAML:

mysql:
    user:
        pass: secret

Python:

import os
import yaml
with open('./config.yml') as file:
    config = yaml.safe_load(file)
print(config['mysql']['user']['pass']) # <--

我更喜欢像这样使用点表示法(dot notation):

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)

这是一个“好”的解决方案吗?如果不是,有更好的替代方案吗?

附加问题[已解决]

我决定使用Ilja Everilä的解决方案。但现在我有一个附加问题:如何创建一个包装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))

更好的解决方案(Ilja Everilä)

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

2
在任何真实的应用程序中,请不要使用模板引擎进行这项工作,因为这是一种非常糟糕的hack。 - ThiefMaster
2
这个链接似乎与问题相关,甚至可能是一个重复的问题。 - ThiefMaster
7个回答

18

简单的

您可以使用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 解析器和工具,例如此答案中提到的工具。


繁琐的方法

轻松一点(不要太认真地看待),您可以创建一个包装器,允许使用属性访问:

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'

1
具体情况可能因人而异,但我认为将模板库作为配置访问的依赖项很臃肿。 - Ilja Everilä
这个解决方案比滥用模板引擎要干净得多 - ThiefMaster
这个最终解决方案真的很棒。非常感谢! - Lugaxx
1
您正在使用“旧”的调用超类方法的方式。请将DotDict.__init__(yaml.safe_load(file))替换为super().__init__(yaml.safe_load(file)。在原始代码中,您将已加载的配置作为self传递给了DotDict.__init__。在某些情况下,显式调用一些超类的方法可能是有用的,但在这种情况下可能并不需要。 - Ilja Everilä
能否请给出负评的人解释一下回答有什么问题,以便进行更正? - Ilja Everilä
显示剩余4条评论

3
我最终使用了python-box。这个包提供了多种读取配置文件的方式(如yaml、csv、json等)。而且,它不仅可以让你直接传递dict或字符串:
from box import Box
import yaml # Only required for different loaders

# Pass dict directly
movie_box = Box({ "Robin Hood: Men in Tights": { "imdb stars": 6.7, "length": 104 } })

# Load from yaml file
# Here it is also possible to use PyYAML arguments, 
# for example to specify different loaders e.g. SafeLoader or FullLoader
conf = Box.from_yaml(filename="./config.yaml", Loader=yaml.FullLoader) 

conf.mysql.user.pass

还有很多例子可在Wiki中找到。


3
一方面,您的示例采用了正确的方法,使用 get_config_value('mysql.user.pass', config) 而不是通过属性解决点访问。我不确定您是否有意识到这一点,您没有尝试更直观的方法:
print(config.mysql.user.pass)

即使您使用过载__getattr__,也无法使其工作,因为pass是Python语言元素。

然而,您的示例仅描述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. 这适用于字符串和整数作为映射键,但可以轻松扩展以支持其他键类型(布尔值、日期、日期时间)。

¹ 这是使用 ruamel.yaml 进行的,它是一个YAML 1.2解析器,我是它的作者。


一个方便的封装程序可以使用 ruamel.yamlpython-box,请参见此答案 - evolved

2

这是一个比较老的问题,但我来到这里寻找答案,希望能找到更简单的解决方案。最终,我使用了easydict库提出了自己的解决方案,可以通过pip install easydict命令进行安装。

  def yaml_load(fileName):
    import yaml
    from easydict import EasyDict as edict
    fc = None
    with open(fileName, 'r') as f:
      fc = edict(yaml.load(f))
      ## or use safe_load
      ## fc = edict(yaml.safe_load(f))

    return fc

现在,只需使用有效的yaml文件名调用yaml_load即可:
config = yaml_load('./config.yml')

## assuming: config["mysql"]["user"]["pass"] is a valid key in config.yml
print("{}".format(config.mysql.user.pass))

1

我之前也遇到过同样的问题,然后编写了这个getter:

 def get(self, key):
    """Tries to find the configuration value for a given key.
    :param str key: Key in dot-notation (e.g. 'foo.lol').
    :return: The configuration value. None if no value was found.
    """
    try:
        return self.__lookup(self.config, key)
    except KeyError:
        return None

def __lookup(self, dct, key):
    """Checks dct recursive to find the value for key.
    Is used by get() interanlly.
    :param dict dct: The configuration dict.
    :param str key: The key we are looking for.
    :return: The configuration value.
    :raise KeyError: If the given key is not in the configuration dict.
    """
    if '.' in key:
        key, node = key.split('.', 1)
        return self.__lookup(dct[key], node)
    else:
        return dct[key]

获取器以递归方式(使用 __lookup)从 self.config 查找配置值。如果您在调整此过程时遇到问题,请随时寻求进一步帮助。

0
Meta/Facebook的hydra库在这里可能过于复杂。Meta/Facebook的hydra库的基础是omegaconf,它可能满足您的需求。它经过了战斗测试并且准备就绪,并将所有内容存储在ConfDict中,我猜测它们是哈希映射,因此轻量且快速。
from omegaconf import OmegaConf

conf = OmegaConf.load(floyd_yaml_path)

print(conf.mysql.user.pass)

0

我通常遵循一种最佳实践,即将配置(任何类型,不仅限于yaml)转换为内存对象

这样,基于文本的配置只需通过一个函数解包,文本就被丢弃了,而我们得到了一个美丽的对象来处理,而不是让每个函数都处理配置的内部。这样,所有函数只知道那个内部对象接口。如果从配置文件中添加/重命名/删除任何新参数,则唯一需要更改的函数是加载器函数,它将配置加载到内存对象中。

以下是我为将FloydHub配置yaml文件加载到内存对象中所做的示例。我认为这是一种非常好的设计模式。

首先定义一个配置代表类,如下所示:

class FloydYamlConfig(object):
class Input:
    def __init__(self, destination, source):
        self.destination = destination
        self.source = source

def __init__(self, floyd_yaml_dict):
    self.machine = floyd_yaml_dict['machine']
    self.env = floyd_yaml_dict['env']
    self.description = floyd_yaml_dict['description']
    self.max_runtime = floyd_yaml_dict['max_runtime']
    self.command = floyd_yaml_dict['command']
    self.input = []
    for input_conf in floyd_yaml_dict['input']:
        input_obj = self.Input(destination=input_conf['destination'], source=input_conf['source'])
        self.input.append(input_obj)

def __str__(self):
    input_str = ''
    for input_obj in self.input:
        input_str += '\ndestination: {}\n source: {}'.format(input_obj.destination, input_obj.source)

    print_str = ('machine: {}\n'
                 'env: {}\n'
                 'input: {}\n'
                 'description: {}\n'
                 'max_runtime: {}\n'
                 'command: {}\n').format(
        self.machine, self.env, input_str, self.description, self.max_runtime, self.command)
    return print_str

然后将 yaml 加载 到对象中以供进一步使用:

floyd_conf = read_floyd_yaml_config(args.floyd_yaml_path)

def read_floyd_yaml_config(floyd_yaml_path) -> FloydYamlConfig:
    with open(floyd_yaml_path) as f:
        yaml_conf_dict = yaml.safe_load(f)

    floyd_conf = FloydYamlConfig(yaml_conf_dict)
    # print(floyd_conf)
    return floyd_conf

示例 YAML

# see: https://docs.floydhub.com/floyd_config

machine: gpu2
env: tensorflow-1.0
input:
  - destination: data
    source: abc/datasets/my-data/6
  - destination: config
    source: abc/datasets/my-config/1
description: this is a test
max_runtime: 3600
command: >-
  echo 'hello world'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接