【问题标题】:Dealing with optional python dictionary fields处理可选的 python 字典字段
【发布时间】:2023-02-16 01:26:51
【问题描述】:

我正在处理加载到 Python 词典中的 JSON 数据。其中很多都有可选字段,然后可能包含字典之类的东西。

dictionary1 = 
{"required": {"value1": "one", "value2": "two"},
"optional": {"value1": "one"}}

dictionary2 = 
{"required": {"value1": "one", "value2": "two"}}

如果我这样做,

dictionary1.get("required").get("value1")

显然,这是有效的,因为字段 "required" 始终存在。

但是,当我在 dictionary2 上使用同一行(以获取可选字段)时,这将产生一个 AttributeError

dictionary2.get("optional").get("value1")
AttributeError: 'NoneType' object has no attribute 'get'

这是有道理的,因为第一个 .get() 将返回 None,而第二个 .get() 不能在 None 对象上调用 .get()

我可以通过提供默认值来解决这个问题,以防可选字段丢失,但是数据变得越复杂,这就越烦人,所以我称之为“天真的修复”:

dictionary2.get("optional", {}).get("value1", " ")

所以第一个.get() 将返回一个空字典{},可以在其中调用第二个.get(),并且由于它显然不包含任何内容,它将返回空字符串,如第二个默认值所定义的那样。

这将不再产生错误,但我想知道是否有更好的解决方案 - 特别是对于更复杂的情况(value1 包含一个数组或另一个字典等......)

我也可以用 try - 除了AttributeError 来解决这个问题,但这也不是我的首选方式。

try:
    value1 = dictionary2.get("optional").get("value1")
except AttributeError:
    value1 = " "

我也不喜欢检查可选字段是否存在,这会产生垃圾代码行,例如

optional = dictionary2.get("optional")
if optional:
    value1 = optional.get("value1")
else:
    value1 = " "

这似乎非常非Pythonic ...

我在想也许我只是链接 .get()s 的方法首先是错误的?

【问题讨论】:

  • 为什么不喜欢 try-except 子句?这是处理此问题的最 pythonic 方式,也是最容易阅读的方式。
  • 只是创建代码行......我想我正在寻找一个单行代码。 (代码中会布满 try-except 块)
  • @c8999c3f964f64 感谢您分享您的解决方案,但通常应将其作为答案发布,而不是对问题进行编辑。我删除了它,但随时将其作为答案发布。谢谢。

标签: python json dictionary attributeerror


【解决方案1】:

在您的代码中:

try:
    value1 = dictionary2.get("optional").get("value1")
except AttributeError:
    value1 = " "

您可以使用括号和except KeyError

try:
    value1 = dictionary2["optional"]["value1"]
except KeyError:
    value1 = " "

如果这对调用者来说太冗长,请添加一个帮助程序:

def get_or_default(d, *keys, default=None):
    try:
        for k in keys:
            d = d[k]
    except (KeyError, IndexError):
        return default
    return d

if __name__ == "__main__":
    d = {"a": {"b": {"c": [41, 42]}}}
    print(get_or_default(d, "a", "b", "c", 1)) # => 42
    print(get_or_default(d, "a", "b", "d", default=43)) # => 43

您还可以将 dict 子类化并使用元组括号索引,例如 NumPy 和 Pandas:

class DeepDict(dict):
    def __init__(self, d, default=None):
        self.d = d
        self.default = default

    def __getitem__(self, keys):
        d = self.d
        try:
            for k in keys:
                d = d[k]
        except (KeyError, IndexError):
            return self.default
        return d

    def __setitem__(self, keys, x):
        d = self.d
        for k in keys[:-1]:
            d = d[k]
        d[keys[-1]] = x

if __name__ == "__main__":
    dd = DeepDict({"a": {"b": {"c": [42, 43]}}}, default="foo")
    print(dd["a", "b", "c", 1]) # => 43
    print(dd["a", "b", "c", 11]) # => "foo"
    dd["a", "b", "c", 1] = "banana"
    print(dd["a", "b", "c", 1]) # => "banana"

但是,如果其他开发人员感到困惑,那么这可能会产生工程成本,并且您想要充实其他预期的方法,如 How to "perfectly" override a dict? 中所述(将其视为概念验证草图)。最好不要太聪明。

【讨论】:

    【解决方案2】:

    您可以为此使用toolz.dicttoolz.get_in()

    from toolz.dicttoolz import get_in
    
    dictionary1 = {"required": {"value1": "one", "value2": "two"}, "optional": {"value1": "one"}}
    dictionary2 = {"required": {"value1": "one", "value2": "two"}}
    
    get_in(("optional", "value1"), dictionary1)
    # 'one'
    
    get_in(("optional", "value1"), dictionary2)
    # None
    
    

    如果你不想安装整个库,你可以copy the source 在 BSD 下获得许可:

    import operator
    from functools import reduce
    
    def get_in(keys, coll, default=None, no_default=False):
        try:
            return reduce(operator.getitem, keys, coll)
        except (KeyError, IndexError, TypeError):
            if no_default:
                raise
            return default
    

    【讨论】:

    • 这似乎非常接近我正在寻找的东西!我会考虑使用这个库,非常感谢!
    【解决方案3】:

    首先,您将 " " 称为空字符串。这是不正确的; "" 是空字符串。

    其次,如果您要检查成员资格,我认为首先没有理由使用 get 方法。我会选择以下内容。

    if "optional" in dictionary2:
        value1 = dictionary2["optional"].get("value1")
    else:
        value1 = ""
    

    要考虑的另一种选择(因为您经常使用 get 方法)是切换到 defaultdict 类。例如,

    from collections import defaultdict
    
    dictionary2 = {"required": {"value1": "one", "value2": "two"}}
    ddic2 = defaultdict(dict,dictionary2)
    value1 = ddic2["optional"].get("value1")
    

    【讨论】:

    • 在空字符串上正确,我的错。首先检查 optional 是否在字典中会创建很多额外的代码行。这变得有点难以管理,检查越多......
    • @c 查看我的编辑:开始使用 defaultdicts 可能会有所帮助。
    • defaultdict 看起来不错,你能解释一下为什么它不能与 .get("optional") 一起使用,但只有在我使用 ["optional"] 时才能使用吗?
    • @c8999c3f964f64 是的,defaultdict 没有“get”方法;当密钥不存在时您获得的替代方案自动成为 ddic2["optional"] 调用的一部分
    • 我实际上要回答这个问题,因为成员资格检查可以用作三元运算符,它完全绕过语句的评估,并且可以变成一个光滑的单行,就像我在寻找value1 = dictionary2["optional"]["value1"] if "optional" in dictionary2 else " "
    【解决方案4】:

    Pythonic 的方式是使用 try/except 块 -

    dictionary2 = {"required": {"value1": "one", "value2": "two"}}
    try:
        value1 = dictionary2["optional"]["value1"]
    except (KeyError, AttributeError) as e:
        value1 = ""
    

    KeyError 用于捕获丢失的键,AttributeError 用于捕获具有 list/str 而不是 dict 对象的情况。


    如果您不喜欢代码中的大量 try/except,您可以考虑使用辅助函数 -

    def get_val(data, keys):
        try:
            for k in keys:
                data = data[k]
            return data
        except (KeyError, AttributeError) as e:
            return ""
    
    dictionary2 = {"required": {"value1": "one", "value2": "two"}}
    print(get_val(dictionary2, ("required", "value2")))
    print(get_val(dictionary2, ("optional", "value1")))
    

    输出 -

    two
    
    

    【讨论】:

    • 我会考虑辅助函数的想法,但是真的没有内置的方法吗,比如 dictionary2.get("optional").get("value1") 或 " ",它忽略了链接的 .get( ) 并在缺少可选字段的情况下返回“或”值?谢谢你的回答!
    • 在那种情况下,最好的 pythonic 方法是按照你的建议之一继续做 dictionary2.get("optional", {}).get("value1", "")
    猜你喜欢
    • 1970-01-01
    • 2019-07-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多