【问题标题】:Python: Accessing YAML values using "dot notation"Python:使用“点表示法”访问 YAML 值
【发布时间】:2017-01-20 16:41:21
【问题描述】:

我正在使用 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']) # <--

我更喜欢这样的东西(点符号):

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))

【问题讨论】:

标签: python python-3.x yaml


【解决方案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 解析器和工具,例如 this answer


错综复杂的

在一个轻松的注释(不要太认真)上,您可以创建一个允许使用属性访问的包装器:

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'

【讨论】:

  • YMMV,我认为有一个模板库作为配置访问膨胀的依赖项。
  • 这个解决方案比为此滥用模板引擎要干净得多
  • @Lugaxx:请记住,您可以使用DotDict (config = DotDict(config)) 包装一次配置对象,然后在代码中的其他任何地方都可以简单地使用config.asdf.asdf.qwer。它不会比这更短。
  • 这个最终解决方案真的很棒。非常感谢!
  • 您正在使用调用超类方法的“旧”风格。将DotDict.__init__(yaml.safe_load(file)) 替换为super().__init__(yaml.safe_load(file)。在您的原始文件中,您调用了DotDict.__init__,加载的配置为self。显式调用一些超类方法可能很有用,但在这种情况下可能不行。
【解决方案2】:

前段时间我遇到了同样的问题并构建了这个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]

getter 以递归方式(使用__lookup)从self.config 查找配置值。 如果您无法根据自己的情况调整此设置,请随时寻求进一步的帮助。

【讨论】:

    【解决方案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.yaml的便捷包装器是python-box,参见answer
    【解决方案4】:

    这是一个很老的问题,但我来这里是为了寻找答案,但在寻找更简单的解决方案。最后,使用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 filename 调用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))
    

    【讨论】:

      【解决方案5】:

      我通常遵循将配置(任何类型,不仅仅是 yaml)转换为内存对象的最佳实践。

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

      以下是我为将 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'
      

      【讨论】:

        【解决方案6】:

        我最终使用了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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-01-31
          • 2020-12-24
          • 1970-01-01
          • 1970-01-01
          • 2012-04-04
          • 2021-04-28
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多