代码之家  ›  专栏  ›  技术社区  ›  John Keyes

复合字典键

  •  0
  • John Keyes  · 技术社区  · 15 年前

    我有一个特殊的例子,使用复合字典键可以使任务更简单。我有一个可行的解决方案,但感觉不太好。你会怎么做?

    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 回复  |  直到 15 年前
        1
  •  2
  •   SilentGhost    15 年前
    >>> 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函数的工作方式,cf。 getattr , operator.getitem 等。

        2
  •  2
  •   Alex Martelli    15 年前

    由于规范中固有的模糊性,接受的解决方案(以及我的第一次尝试)失败: '.' 可能是“只是一个分隔符” 实际键字符串的一部分。例如,考虑一下 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 等等),但有一种情况是这样的:很难检查一个整数是否是一个可以接受的索引/键,它可以是一个dict或一个列表,而不需要进行一些自省来区分这些情况, (这里看起来更简单)a 尝试/排除 因此,我选择了后者,简单性一直是我最关心的问题。总之…

    我相信这种方法的变体(在任何时候可能仍然可行的所有“可能的连续上下文对”都保留在这里)是处理我上面解释过的模棱两可的唯一工作方式(当然,可以选择收集 全部的 可能的解决方案,根据任何启发式标准任意选择其中一个,或者如果歧义被咬,可能会提高,因此有多个解决方案等,但这些都是这个一般想法的微小变体)。

        3
  •  1
  •   John    15 年前

    我将把我最初的解决方案留给子孙后代:

    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
    

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

        4
  •  0
  •   johntellsall    15 年前

    以下代码有效。它检查是否有一个特殊情况下的一个关键有一个周期在它。然后,它将钥匙分开。对于每个子键,它尝试从类似列表的上下文中获取值,然后尝试从字典类型的上下文中获取值,然后放弃。

    此代码还显示了如何使用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)
    
        5
  •  0
  •   John Keyes    15 年前

    作为关键 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]
    

    关于这是否是一个改进,我持怀疑态度。