当我们添加类型提示时,我们会发现我们对严格性的渴望与 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 #1776将str
改为Any
,并解释了原因。核心的Logger
方法使用str(msg)
强制将msg
变成一个字符串,这意味着它允许任何类型。但使用非str
类型可能代表一个错误,使日志信息无用,Guido van Rossum 在 typeshed PR 上对此表示遗憾。
在这种特殊情况下,我还是很难过看到日志msg参数从str变成Any,因为这将减少捕捉错误的机会。 根据我的经验,*通常*这是个编码错误。
噢,糟了。
3.json.loads()
和朋友
JSON有一个特别有限的类型集,这似乎可以很好地转化为json.loads()
的类型提示。有四个原子类型。
null
– 被加载为None
- 布尔–加载为
bool
- 数字–以
int
s或float
s的形式加载 - 字符串 – 作为
str
s加载
…还有两个容器类型。
- 数组–加载为
list
s - 对象–以
dict
s的形式加载,并带有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
。
阿丹