处理可选的 python 字典字段

Dealing with optional python dictionary fields

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

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

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

如果我这样做,

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

这显然有效,因为“必填”字段始终存在。

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

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

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

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

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

因此第一个 .get() 将 return 一个空字典 {},可以在其上调用第二个 .get(),并且由于它显然不包含任何内容,因此它将 return根据第二个默认值定义的空字符串。 这将不再产生错误,但我想知道是否有更好的解决方案 - 特别是对于更复杂的情况(value1 包含一个数组或另一个字典等......)

我也可以用 try - except 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 的方法一开始就是错误的?


编辑:感谢 Ben Grossmann 的回答,我想出了涵盖我的用例的这个单行代码。

value1 = dictionary2["optional"]["value1"] if "optional" in dictionary2 else " "

作为三元运算符的成员检查将绕过导致错误的语句的评估并使用默认值“”,而不必为各个检查提供默认值

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

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

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

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

from collections import defaultdict

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

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

在您的代码中:

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? 中描述的其他预期方法(将其视为 proof-of-concept草图)。最好不要太聪明

您可以为此使用 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