复合字典键
我有一个特定的情况,使用复合字典键会让任务变得更简单。我现在有一个可行的解决方案,但觉得它不太优雅。你会怎么做呢?
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 个回答
我把我最初的解决方案留着,以备后用:
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
不过我不太确定这种方法有什么好处。
这个被接受的解决方案(还有我最初的尝试)失败了,主要是因为规范本身有些模糊:'.'
可能只是一个分隔符,也可能是实际键字符串的一部分。举个例子,假设 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
等进行的反射),但有一个情况还是回来了:很难判断一个整数是否是字典或列表中可接受的索引/键,而不进行一些反射来区分这些情况,或者(在这里看起来更简单)使用 try/except
,所以我选择了后者,简单性始终是我关注的重点之一。总之……
我相信这种方法的变体(在任何时刻都保留所有可能的“继续上下文对”)是处理我上面提到的模糊性的唯一有效方法。当然,人们也可以选择收集所有可能的解决方案,根据任何想要的启发式标准随便挑一个,或者如果模糊性导致有多个解决方案时可能抛出异常等等,但这些都是这个大思路的小变体。
>>> 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
等等。