复合字典键

时间:2010-05-15 22:27:07

标签: attributes python dictionary

我有一个特殊情况,使用复合词典键可以使任务更容易。我有一个有效的解决方案,但觉得它不够优雅。你会怎么做?

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 个答案:

答案 0 :(得分:2)

>>> 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函数的工作方式,参见getattroperator.getitem

答案 1 :(得分:2)

由于规范中固有的模糊性,已接受的解决方案(以及我的第一次尝试)失败:'.'可能只是“分隔符”是实际密钥的一部分串。例如,请考虑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(以及通过hasattrisinstance等进行递归和内省),但是一个人回过头来in:很难检查一个整数是否是一个可接受的索引/键可能是一个dict或list,而没有一些内省来区分这些情况,(这里看起来更简单)一个try/except,所以我最后一步,简单总是接近我的担忧。总之...

我相信这种方法的变体(所有“可能的延续 - 上下文对”在任何时候都可以保持可行)是处理我上面解释的模糊性的唯一工作方式(当然,人们可能会选择收集所有可能的解决方案,根据任何启发式标准任意选择其中一个解决方案,或者如果歧义是咬人的话可能会提出,所以有多种解决方案等,但这些是这个一般想法的小变种。)

答案 2 :(得分:1)

我将离开原始解决方案给后人:

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

但这是一个实现类似于doublep建议的getitem接口的方法的版本。我特别没有处理虚线键,而是强制键进入单独的嵌套结构,因为这对我来说似乎更清晰:

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

我不确定这种方法的优点是什么。

答案 3 :(得分:0)

以下代码有效。它检查具有句点的单个键的特殊情况。然后,它将键分开。对于每个子键,它尝试从类似列表的上下文中获取值,然后从字典类型上下文中尝试,然后放弃。

此代码还说明了如何使用unittest / nose,这是高度推荐的。用“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)

答案 4 :(得分:0)

由于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]

我对这是否有所改善持怀疑态度。