是否可以确定包含 None 的嵌套字典中的哪个 level/key,导致 'NoneType' 对象不可订阅?

Is it possible to determine which level/key of a nested dict that contained None, causing 'NoneType' object is not subscriptable?

我的框架的用户(他们可能精通也可能不精通 Python)编写导航字典的代码(最初来自某些 [=35= 的 json 响应]).

有时他们会犯错误,有时 API returns 数据会缺失一些值,他们会得到可怕的 'NoneType' object is not subscriptable

如何明确错误发生在什么级别? (返回什么键 None)

def user_code(some_dict):
    # I can't modify this code, it is written by the user
    something = some_dict["a"]["b"]["c"]

# I don't control the contents of this.
data_from_api = '{"a": {"b": None}}'

# framework code, I control this
try:
    user_code(json.loads(data_from_api))
except TypeError as e:
    # I'd like to print an error message containing "a","b" here

如有必要,我可以overload/alter dict 实现,但我不想进行源代码检查。

这个问题可能已经有了答案(或者不可能),但是在所有基本问题中很难找到 为什么我得到 'NoneType' object is not subscriptable ? 个问题。如果这是重复的,我很抱歉。

编辑:@2e0byo 的回答对我原来的问题是最正确的,但我确实发现 autoviv 为我的“真实”潜在问题提供了一个很好的解决方案(允许用户轻松浏览有时没有所有预期的数据),所以我选择了这种方法。它唯一真正的缺点是如果有人依赖 some_dict["a"]["b"]["c"] 来抛出异常。我的解决方案是这样的:

def user_code(some_dict):
    # this doesnt crash anymore, and instead sets something to None
    something = some_dict["a"]["b"]["c"]

# I don't control the contents of this.
data_from_api = '{"a": {"b": None}}'

# framework code, I control this
user_code(autoviv.loads(data_from_api))

这是解决此问题的一种方法:使您的代码 return 成为一个自定义 Result() 对象来包装每个对象。 (这种方法可以推广到 .left().right() 的 monad 方法,但我没有去那里,因为我不经常看到这种模式(在我公认的小经验中!)。

示例代码

首先是自定义 Result() 对象:

class Result:
    def __init__(self, val):
        self._val = val

    def __getitem__(self, k):
        try:
            return self._val[k]
        except KeyError:
            raise Exception("No such key")
        except TypeError:
            raise Exception(
                "Result is None.  This probably indicates an error in your code."
            )

    def __getattr__(self, a):
        try:
            return self._val.a
        except AttributeError:
            if self._val is None:
                raise Exception(
                    "Result is None.  This probably indicates an error in your code."
                )
            else:
                raise Exception(
                    f"No such attribute for value of type {type(self._val)}, valid attributes are {dir(self._val)}"
                )

    @property
    def val(self):
        return self._val

当然,这里还有很大的改进空间(例如__repr__(),您可能想修改错误消息)。

在行动:

def to_result(thing):
    if isinstance(thing, dict):
        return Result({k: to_result(v) for k, v in thing.items()})
    else:
        return Result(thing)

d = {"a": {"b": None}}
r_dd = to_result(d)
r_dd["a"] # Returns a Result object
r_dd["a"]["b"] # Returns a Result object
r_dd["a"]["c"] # Raises a helpful error
r_dd["a"]["b"]["c"] # Raises a helpful error
r_dd["a"]["b"].val # None
r_dd["a"]["b"].nosuchattr # Raises a helpful error

推理

如果我要提供一个自定义对象,我希望我的用户知道它是一个自定义对象。所以我们有一个包装器 class,我们告诉用户范式是 'get at the object, and then use .val to get the result'。处理错误的 .val 是 他们的 代码的问题(所以如果 .val 是 None,他们必须处理那个)。但是处理数据结构中的问题是我们的问题,所以我们给他们一个定制的 class,其中包含有用的消息,而不是其他任何东西。

获取嵌套错误的级别

按照目前的实现,很容易在错误消息中得到上面的一个(用于 dict 查找)。如果你想获得更多,你需要在 Result 的层次结构中保留一个引用——这可能更好地用 Result 编写,而不仅仅是一个包装器。

我不确定这是否是您正在寻找的解决方案,但这可能是朝着正确方向迈出的一步。