使用 mypy / PEP-526 定义一个 jsonable 类型

Define a jsonable type using mypy / PEP-526

可以通过 json.dumps 转换为 JSON 字符串的值是:

Union[str, int, float, Mapping, Iterable]

你有更好的建议吗?

长话短说,您有以下选择:

  1. 如果您对 JSON 的结构以及必须支持任意 JSON blob 的方式一无所知,您可以:
    1. 等待 mypy 支持递归类型。
    2. 如果等不及,就用objectDict[str, object]。它最终与在实践中使用递归类型几乎相同。
    3. 如果您不想经常对代码进行类型检查,请使用 AnyDict[str, Any]。这样做可以避免以类型安全为代价进行大量 isinstance 检查或强制转换。
  2. 如果您确切知道 JSON 数据是什么样的,您可以:
    1. 使用 TypedDict
    2. 使用像 Pydantic 这样的库将你的 JSON 反序列化为一个对象

更多讨论如下。

情况 1:您不知道 JSON 的结构

不幸的是,正确输入任意 JSON blob 对于 PEP 484 类型来说很尴尬。这部分是因为 mypy(当前)缺少递归类型:这意味着我们能做的最好的事情就是使用与您构造的类型相似的类型。

(但是,我们可以对您的类型进行一些改进。特别是,json.Dumps(...) 实际上不接受任意迭代。例如,生成器是 Iterable 的子类型,但是json.dumps(...) 将拒绝序列化生成器。您可能想使用类似 Sequence 的东西。)

也就是说,访问递归类型可能最终也没有太大帮助:为了使用这种类型,您需要开始在代码中加入 isinstance 检查或强制转换。例如:

JsonType = Union[None, int, str, bool, List[JsonType], Dict[JsonType]]

def load_config() -> JsonType:
    # ...snip...

config = load_config()
assert isinstance(config, dict)

name = config["name"]
assert isinstance(name, str)

那么如果是这样的话,我们真的需要递归类型的完整精度吗?在大多数情况下,我们可以只使用 objectDict[str, object]:无论哪种情况,我们在运行时编写的代码都几乎相同。

例如,如果我们将上面的示例更改为使用 JsonType = object,我们最终仍然需要两个断言。

或者,如果您发现 assert/isinstance 检查对于您的用例来说是不必要的,第三种选择是使用 AnyDict[str, Any] 并让您的 JSON 动态输入。

它显然不如上面提供的选项精确,但要求 mypy 不对 JSON dict 的使用进行类型检查,而是依赖运行时异常有时在实践中更符合人体工程学。

案例 2:您知道 JSON 数据的结构

如果您不需要支持任意 JSON blob 并且可以假设它形成特定形状,我们还有更多选择。

第一个选项是使用 TypedDicts。基本上,您构造一个类型,明确指定特定的 JSON blob 应该是什么样子,然后使用它。这需要做更多的工作,但是可以让你获得更多的类型安全。

使用 TypedDicts 的主要缺点是它最终基本上相当于一个巨大的转换。例如,如果您这样做:

from typing import TypedDict
import json

class Config(TypedDict):
    name: str
    env: str

with open("my-config.txt") as f:
    config: Config = json.load(f)

...我们怎么知道 my-config.txt 实际上匹配这个 TypedDict?

嗯,我们不知道,不确定。

如果您可以完全控制 JSON 的来源,这可能没问题。在这种情况下,不去验证传入的数据可能会很好:只需让 mypy 检查 uses 你的字典就足够了。

但是如果进行运行时验证对您很重要,您的选择是自己实现该验证逻辑或使用可以代表您执行此操作的第 3 方库,例如 Pydantic:

from pydantic import BaseModel
import json

class Config(BaseModel):
    name: str
    env: str

with open("my-config.txt") as f:
    # The constructor will raise an exception at runtime
    # if the input data does not match the schema
    config = Config(**json.load(f))

使用这些类型的库的主要优点是您可以获得完整的类型安全性。您还可以使用对象属性语法而不是字典查找(例如 config.name 而不是 config["name"]),这可以说更符合人体工程学。

主要缺点是执行此验证确实会增加一些运行时成本,因为您现在正在扫描整个 JSON blob。如果您的 JSON 恰好包含大量数据,这可能最终会给您的代码带来一些不小的减速。

将数据转换为对象有时也会有点不方便,尤其是当您打算稍后将其转换回字典时。

关于引入 JSONType 的可能性进行了长时间的讨论 (https://github.com/python/typing/issues/182);不过目前还没有定论。

目前的建议是在您自己的代码中定义 JSONType = t.Union[str, int, float, bool, None, t.Dict[str, t.Any], t.List[t.Any]] 或类似的东西。