Python类型提示–Python标准库中typing.Any的三种意想不到的用法

当我们添加类型提示时,我们会发现我们对严格性的渴望与 Python 的灵活性相矛盾。 在这篇文章中,我们将探讨标准库中的三组函数,我天真地以为它们会使用窄类型,但由于一些边缘情况,反而使用Any

1.operator 函数

operator 模块为 Python 的运算符提供了包装函数。例如,operator.gt(a, b) 包装了 “大于 “运算符,所以相当于a > b

我们通常期望运算符有很好的定义类型。 例如,像> 这样的比较运算符返回bool 的值,正如语法文档中所说。

但是 Python 允许运算符重载,这使得自定义运算符函数可以返回任意类型。pathlib.Path 做到了这一点,效果很好,使用除法运算符/ 进行连接。

这种灵活性迫使operator 模块函数的类型接受并返回Any 。这里是operator 中比较函数的当前 typeshed stubs

def lt(__a: Any, __b: Any) -> Any: ...
def le(__a: Any, __b: Any) -> Any: ...
def eq(__a: Any, __b: Any) -> Any: ...
def ne(__a: Any, __b: Any) -> Any: ...
def ge(__a: Any, __b: Any) -> Any: ...
def gt(__a: Any, __b: Any) -> Any: ...
复制代码

考虑到operator 的灵活性,我们可能会发现为我们的特定用例重新实现窄类型的函数更好。 例如。

def gt(a: int, b: int) -> bool:
    return a > b
复制代码

2.logging 模块

Python 的logging 模块被记录为接收_字符串_日志信息。

msg是消息格式字符串,args是参数,使用字符串格式化操作符将其合并到msg中。

所以我们可以合理地期望Logger.debug() 和co.的类型提示都将msg 定义为str 。但实际上所有的方法都将msg 定义为Any 。为什么?

typeshed PR #1776str 改为Any ,并解释了原因。核心的Logger 方法使用str(msg) 强制将msg 变成一个字符串,这意味着它允许任何类型。但使用非str 类型可能代表一个错误,使日志信息无用,Guido van Rossum 在 typeshed PR 上对此表示遗憾。

在这种特殊情况下,我还是很难过看到日志msg参数从str变成Any,因为这将减少捕捉错误的机会。 根据我的经验,*通常*这是个编码错误。

噢,糟了。

3.json.loads() 和朋友

JSON有一个特别有限的类型集,这似乎可以很好地转化为json.loads() 的类型提示。有四个原子类型。

  • null – 被加载为None
  • 布尔–加载为bool
  • 数字–以ints或floats的形式加载
  • 字符串 – 作为strs加载

…还有两个容器类型。

  • 数组–加载为lists
  • 对象–以dicts的形式加载,并带有str 键。

容器类型可以包含任何原子类型_或_其他容器。

这种容器-可以包含-容器的递归是我们在类型提示中表示JSON的第一个问题。 我们需要使用递归类型提示,不幸的是Mypy目前不支持。 如果我们尝试递归定义JSON类型,像这样。

from typing import Dict, List, Union

_PlainJSON = Union[
    None, bool, int, float, str, List["_PlainJSON"], Dict[str, "_PlainJSON"]
]
JSON = Union[_PlainJSON, Dict[str, "JSON"], List["JSON"]]
复制代码

…Mypy将报告 “可能的循环定义 “错误。

$ mypy example.py
example.py:3: error: Cannot resolve name "_PlainJSON" (possible cyclic definition)
example.py:4: error: Cannot resolve name "_PlainJSON" (possible cyclic definition)
example.py:6: error: Cannot resolve name "JSON" (possible cyclic definition)
Found 3 errors in 1 file (checked 1 source file)
复制代码

在Mypy中对递归类型的支持是可行的,并在其问题#731中被跟踪。

但是,即使Mypy增加了对递归类型的支持,json.loads() ,仍然需要使用Any 的返回类型。这又是由于其API的额外灵活性。

json.loads() 接受几个额外的参数,这些参数可以用来改变JSON的类型以加载到不同的Python类型中。值得注意的是,cls 参数允许完全替换加载机制,所以我们可以让JSON解析成_任何_类型。 因此,json.loads() 总是需要一个返回类型为Any

其他格式的库,如PyYAML,也采用了同样的模式。 因此它们也使用Any 的返回类型。

总结

我们已经看到,讨厌的Any 类型可能 “隐藏 “在通常具有良好类型的 API 中,但提供了一些灵活性。 在使用这些函数时,我们需要小心。

随着类型提示在 Python 生态系统中的传播,我们可能会看到这样的 API 被改变,以便在常见的情况下允许更严格的类型。例如,json.loads() 可以被分成两个函数:一个提供没有灵活性的、定义良好的返回类型,另一个提供所有自定义的、返回类型为Any

愿你的类型提示带你到你想去的地方Any

阿丹

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享