复合字典键

0 投票
5 回答
1646 浏览
提问于 2025-04-15 22:45

我有一个特定的情况,使用复合字典键会让任务变得更简单。我现在有一个可行的解决方案,但觉得它不太优雅。你会怎么做呢?

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

我把我最初的解决方案留着,以备后用:

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

不过我不太确定这种方法有什么好处。

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 等进行的反射),但有一个情况还是回来了:很难判断一个整数是否是字典或列表中可接受的索引/键,而不进行一些反射来区分这些情况,或者(在这里看起来更简单)使用 try/except,所以我选择了后者,简单性始终是我关注的重点之一。总之……

我相信这种方法的变体(在任何时刻都保留所有可能的“继续上下文对”)是处理我上面提到的模糊性的唯一有效方法。当然,人们也可以选择收集所有可能的解决方案,根据任何想要的启发式标准随便挑一个,或者如果模糊性导致有多个解决方案时可能抛出异常等等,但这些都是这个大思路的小变体。

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等等。

撰写回答