Составные словарные ключи - PullRequest
0 голосов
/ 16 мая 2010

У меня есть особый случай, когда использование составных словарных ключей облегчит задачу. У меня есть рабочее решение, но я чувствую, что оно не элегантное. Как бы вы это сделали?

context = {
    'database': {
        'port': 9990,
        'users': ['number2', 'dr_evil']
    },
    'admins': ['number2@virtucon.com', 'dr_evil@virtucon.com'],
    'domain.name': 'virtucon.com'
}

def getitem(key, context):
    if hasattr(key, 'upper') and key in context:
        return context[key]

    keys = key if hasattr(key, 'pop') else key.split('.')

    k = keys.pop(0)
    if keys:
        try:
            return getitem(keys, context[k])
        except KeyError, e:
            raise KeyError(key)
    if hasattr(context, 'count'):
        k = int(k)
    return context[k]

if __name__ == "__main__":
    print getitem('database', context)
    print getitem('database.port', context)
    print getitem('database.users.0', context)
    print getitem('admins', context)
    print getitem('domain.name', context)
    try:
        getitem('database.nosuchkey', context)
    except KeyError, e:
        print "Error:", e

Спасибо.

Ответы [ 5 ]

2 голосов
/ 16 мая 2010

Принятое решение (как и моя первая попытка) потерпело неудачу из-за неоднозначности, присущей спецификациям: '.' может быть "просто разделителем" или частью фактической строки ключей. Например, учтите, что key может быть 'a.b.c.d.e.f', а фактический ключ для использования на текущем уровне - 'a.b.c.d' с 'e.f', оставшимся для следующего уровня с самым большим отступом. Кроме того, спецификация неоднозначна в другом смысле: если присутствует более одного префикса с присоединением к точке 'key', какой использовать?

Предположим, что целью является попытка каждого такого возможного префикса: это может привести к нескольким решениям, но мы можем произвольно вернуть первое решение, найденное в этом случае.

def getitem(key, context):
    stk = [(key.split('.'), context)]
    while stk:
      kl, ctx = stk.pop()
      if not kl: return ctx
      if kl[0].isdigit():
        ik = int(kl[0])
        try: stk.append((kl[1:], ctx[ik]))
        except LookupError: pass
      for i in range(1, len(kl) + 1):
        k = '.'.join(kl[:i])
        if k in ctx: stk.append((kl[i:], ctx[k]))
    raise KeyError(key)

Первоначально я пытался избежать всех try/except с (а также рекурсии и самоанализа с помощью hasattr, isinstance и т. Д.), Но одна вещь вернулась: трудно проверить, целое число является приемлемым индексом / ключом к тому, что может быть либо диктовкой, либо списком, без какого-либо самоанализа для различения случаев, или (и здесь это выглядит проще) a try/except, поэтому я пошел В последнем случае простота всегда была в центре моих забот. В любом случае ...

Я полагаю, что варианты этого подхода (где все «возможные пары продолжения-контекста», которые все еще могут быть осуществимы в любой точке) сохраняются) являются единственным рабочим способом устранения неоднозначностей, которые я объяснил выше (конечно, Можно выбрать всех возможных решений, произвольно выбрать одно из них в соответствии с любым эвристическим критерием, по желанию, или повысить, если неоднозначность кусается, поэтому существует несколько решений и т. д., и т. д., но они незначительные варианты этой общей идеи).

2 голосов
/ 16 мая 2010
>>> def getitem(context, key):
    try:
        return context[key]
    except KeyError:
        pass
    cur, _, rest = key.partition('.')
    rest = int(rest) if rest.isdigit() else rest
    return getitem(context[cur], rest)


>>> getitem(context, 'admins.0')
'number2@virtucon.com'
>>> getitem(context, 'database.users.0')
'number2'
>>> getitem(context, 'database.users.1')
'dr_evil'

Я изменил порядок аргументов, потому что так работают большинство функций Python, ср. getattr, operator.getitem и т. Д.

1 голос
/ 16 мая 2010

Я оставляю свое исходное решение для потомков:

CONTEXT = {
    "database": {
        "port": 9990,
        "users": ["number2", "dr_evil"]},
    "admins": ["number2@virtucon.com", "dr_evil@virtucon.com"],
    "domain": {"name": "virtucon.com"}}


def getitem(context, *keys):
    node = context
    for key in keys:
        node = node[key]
    return node


if __name__ == "__main__":
    print getitem(CONTEXT, "database")
    print getitem(CONTEXT, "database", "port")
    print getitem(CONTEXT, "database", "users", 0)
    print getitem(CONTEXT, "admins")
    print getitem(CONTEXT, "domain", "name")
    try:
        getitem(CONTEXT, "database", "nosuchkey")
    except KeyError, e:
        print "Error:", e

Но вот версия, которая реализует подход, аналогичный интерфейсу getitem, предложенному doublep. Я специально не работаю с пунктирными ключами, а скорее делаю ключи в отдельных вложенных структурах, потому что это кажется мне чище:

CONTEXT = {
    "database": {
        "port": 9990,
        "users": ["number2", "dr_evil"]},
    "admins": ["number2@virtucon.com", "dr_evil@virtucon.com"],
    "domain": {"name": "virtucon.com"}}


if __name__ == "__main__":
    print CONTEXT["database"]
    print CONTEXT["database"]["port"]
    print CONTEXT["database"]["users"][0]
    print CONTEXT["admins"]
    print CONTEXT["domain"]["name"]
    try:
        CONTEXT["database"]["nosuchkey"]
    except KeyError, e:
        print "Error:", e

Вы можете заметить, что я действительно здесь сделал, исключил всякую церемонию доступа к структуре данных. Вывод этого скрипта такой же, как и оригинал, за исключением того, что он не содержит точечный ключ. Мне кажется, это более естественный подход, но если вы действительно хотите иметь возможность работать с точечными ключами, вы можете сделать что-то вроде этого:

CONTEXT = {
    "database": {
        "port": 9990,
        "users": ["number2", "dr_evil"]},
    "admins": ["number2@virtucon.com", "dr_evil@virtucon.com"],
    "domain": {"name": "virtucon.com"}}


def getitem(context, dotted_key):
    keys = dotted_key.split(".")
    value = context
    for key in keys:
        try:
            value = value[key]
        except TypeError:
            value = value[int(key)]
    return value


if __name__ == "__main__":
    print getitem(CONTEXT, "database")
    print getitem(CONTEXT, "database.port")
    print getitem(CONTEXT, "database.users.0")
    print getitem(CONTEXT, "admins")
    print getitem(CONTEXT, "domain.name")
    try:
        CONTEXT["database.nosuchkey"]
    except KeyError, e:
        print "Error:", e

Я не уверен, каким было бы преимущество такого подхода.

0 голосов
/ 16 мая 2010

Поскольку ключом к getitem должна быть строка (или список, который передается в рекурсивном вызове), я придумал следующее:

def getitem(key, context, first=True):
    if not isinstance(key, basestring) and not isinstance(key, list) and first:
        raise TypeError("Compound key must be a string.")

    if isinstance(key, basestring):
        if key in context:
            return context[key]
        else:
            keys = key.split('.')
    else:
        keys = key

    k = keys.pop(0)
    if key:
        try:
            return getitem(keys, context[k], False)
        except KeyError, e:
            raise KeyError(key)
    # is it a sequence type
    if hasattr(context, '__getitem__') and not hasattr(context, 'keys'):
        # then the index must be an integer
        k = int(k)
    return context[k]

Я нахожусь на ограждении относительно того, является ли это улучшением.

0 голосов
/ 16 мая 2010

Следующий код работает. Он проверяет особый случай одного ключа с точкой в ​​нем. Затем он разбивает ключ на части. Для каждого подключа он пытается извлечь значение из контекста, подобного списку, затем пытается из контекста словарного типа, затем он сдается.

Этот код также показывает, как использовать юнит-тест / нос, который настоятельно рекомендуется. Тест с помощью "nosetests mysource.py".

Наконец, consder использует встроенный в Python класс ConfigParser, который действительно полезен для задач конфигурации этого типа: http://docs.python.org/library/configparser.html

#!/usr/bin/env python

from nose.tools import eq_, raises

context = {
    'database': {
        'port': 9990,
        'users': ['number2', 'dr_evil']
    },
    'admins': ['number2@virtucon.com', 'dr_evil@virtucon.com'],
    'domain.name': 'virtucon.com'
}

def getitem(key, context):
    if isinstance(context, dict) and context.has_key(key):
        return context[key]
    for key in key.split('.'):
        try:
            context = context[int(key)]
            continue
        except ValueError:
            pass
        if isinstance(context, dict) and context.has_key(key):
            context = context[key]
            continue
        raise KeyError, key
    return context

def test_getitem():
    eq_( getitem('database', context), {'port': 9990, 'users': ['number2', 'dr_evil']} )
    eq_( getitem('database.port', context), 9990 )
    eq_( getitem('database.users.0', context), 'number2' )
    eq_( getitem('admins', context), ['number2@virtucon.com', 'dr_evil@virtucon.com'] )
    eq_( getitem('domain.name', context), 'virtucon.com' )

@raises(KeyError)
def test_getitem_error():
    getitem('database.nosuchkey', context)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...