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

Posted

技术标签:

【中文标题】使用 mypy / PEP-526 定义 jsonable 类型【英文标题】:Define a jsonable type using mypy / PEP-526 【发布时间】:2018-12-19 20:43:10 【问题描述】:

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

标量:数字和字符串 容器:映射和可迭代

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

你有更好的建议吗?

【问题讨论】:

糟糕,我忘记了布尔值——mypy 需要一个标量 元组是可迭代的,但不是 jsonify-able 【参考方案1】:

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

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

更多讨论如下。

案例 1:您不知道 JSON 的结构

不幸的是,使用 PEP 484 类型正确键入任意 JSON blob 很尴尬。这部分是因为 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,我们最终仍然需要两个断言。

或者,如果您发现在您的用例中不需要进行断言/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 检查您的 dict 的 uses 就足够了。

但是,如果运行时验证对您很重要,您可以选择自己实现该验证逻辑或使用可以代表您执行此操作的 3rd 方库,例如 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))

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

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

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

【讨论】:

【参考方案2】:

关于引入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]] 或类似的东西。

【讨论】:

以上是关于使用 mypy / PEP-526 定义 jsonable 类型的主要内容,如果未能解决你的问题,请参考以下文章

对于使用 `type()` 构造的类型,mypy“作为类型无效”

设置 pyflake 和 mypy 忽略同一行

如何让 mypy 抱怨将 Any 分配给 int(第 2 部分)

mypy 无法识别从超类继承的变量

使用 mypy 检查类型时有啥问题

如何定义重载类型签名?