2016-09-13 4 views
5

Я использую конфигурационный файл YAML. Так что это код для загрузки моей конфигурации в Python:Python: Доступ к значениям YAML с использованием «точечной нотации»

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) 

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

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

Я решил использовать решение пользователя Ilya Everilä в. Но теперь у меня есть дополнительный вопрос: как бы вы создали оболочку класса 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__' 

Решение

You просто нужно передать self конструктору суперкласса.

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

Еще лучше soltution (Ilja Everilä)

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

Использование механизма шаблонов для этого - действительно ужасный взлом. Пожалуйста, не делайте этого в каком-либо реальном приложении! – ThiefMaster

+2

http://stackoverflow.com/questions/11049117/how-to-load-a-pyyaml-file-and-access-it-using-attributes-instead-of-using-the-di кажется связанным или даже duplicate – ThiefMaster

ответ

10

Простой

Вы можете использовать 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 

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

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

Извините, но это еще более раздуто, чем нотация скобок ... – Lugaxx

+1

YMMV, я бы назвал наличие библиотеки шаблонов в качестве зависимости для раздувания доступа к конфигурации. –

+0

Это решение ** много ** чище, чем злоупотребление шаблоном для этого. – ThiefMaster

1

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

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] 

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

2

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

print(config.mysql.user.pass) 

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

Однако ваш пример описывает только очень ограниченное подмножество 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, а не только к тем, которые состоят из рекурсивных сопоставлений со строковыми массивами ars как ключи.

  1. Как показано вы можете напрямую позвонить config.string_access( mysql.user.pass ) вместо определения и использования get_config_value()
  2. это работает со строками и целыми числами в качестве ключей отображения, но может быть легко расширена для поддержки других типов ключей (логическое , дата, дата-время).

¹ Это было сделано с помощью ruamel.yaml YAML 1.2, из которых я являюсь автором.

Смежные вопросы