Python: доступ к значениям YAML с использованием «точечной нотации»

Я использую конфигурационный файл 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) 

Будет ли это «хорошим» решением? Если нет, что было бы лучшей альтернативой?

Дополнительный вопрос [Решено]

Я решил использовать решение Илья Эверилья. Но теперь у меня есть дополнительный вопрос: как бы вы создали оболочку класса Config вокруг DotConf?

Следующий код не работает, но я надеюсь, что вы поняли, что я пытаюсь сделать:

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

3 Solutions collect form web for “Python: доступ к значениям YAML с использованием «точечной нотации»”

Простой

Вы можете использовать reduce чтобы извлечь значение из config:

 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 

Обратите внимание, что это не удается для ключевых слов, таких как «как», «пройти», «если» и т. П.

Наконец, вы можете стать действительно сумасшедшим (читайте: возможно, это не очень хорошая идея) и настройте 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' по 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' 

С одной стороны, ваш пример использует правильный подход, используя get_config_value('mysql.user.pass', config) вместо решения точечного доступа с атрибутами. Я не уверен, что вы поняли, что специально не пытаетесь сделать более интуитивно понятным:

 print(config.mysql.user.pass) 

который вы не можете заставить работать, даже когда перегружаете __getattr__ , поскольку pass является элементом языка Python.

Однако в вашем примере описывается только очень ограниченное подмножество файлов YAML, так как оно не содержит никаких коллекций последовательностей и каких-либо сложных ключей.

Если вы хотите покрыть больше, чем крошечное подмножество, вы можете, например, расширить мощные объекты с возможностью ruamel.yaml движения 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 """ config = ruamel.yaml.round_trip_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, автором которого я являюсь.

Некоторое время назад у меня была такая же проблема, и я построил этот геттер:

  def get(self, key): """Tries to find the configuration value for a given key. :param str key: Key in dot-notation (eg '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] 

Геттер просматривает значение конфигурации из self.config рекурсивным образом (используя __lookup ). Если у вас есть проблемы с настройкой для вашего случая, не стесняйтесь обращаться за дополнительной помощью.

  • Ошибки pyYAML в строке «!» в строке
  • YAML-файл url и скрипт в GAE python
  • Почему PyYAML использует генераторы для создания объектов?
  • Как я могу проанализировать YAML-файл в Python
  • Можно ли использовать блоки метаданных yaml для расширения синтаксиса pandoc?
  • Как я могу контролировать, какую скалярную форму PyYAML использует для моих данных?
  • Могу ли я ускорить YAML?
  • Разбор XML или YML в OpenCV с помощью Python
  • Python - лучший язык программирования в мире.