Any
转换。def greeting(name: str) -> str:
return 'Hello ' + name
mypy path/to/file.py
后,Mypy 会把推断出的违规代码都吐出来。Python 在运行时显露但不利用那些类型注解。disallow_untyped_defs
,它要求对所有函数签名进行注解),从那时起,我们一直维护着这些设置。(Wolt 团队有一篇很好的文章,他们称之为“专业级的 Mypy 配置”,巧合的是,我们使用的正是这种配置。)Mypy 配置:https://blog.wolt.com/engineering/2021/09/30/professional-grade-mypy-configuration/
Zulip 博文:https://blog.zulip.com/2016/10/13/static-types-in-python-oh-mypy/#benefitsofusingmypy
mypy.ini
里添加一个许可条目,它告诉 Mypy 要忽略那些模块的类型注解(有类型或提供类型存根的库,比较罕见):[mypy-altair.*]
ignore_missing_imports = True
[mypy-apache_beam.*]
ignore_missing_imports = True
[mypy-bokeh.*]
ignore_missing_imports = True
...
import pandas as pd
def return_data_frame() -> pd.DataFrame:
"""Mypy interprets pd.DataFrame as Any, so returning a str is fine!"""
return "Hello, world!"
functools.lru_cache
尽管在 typeshed 里有类型注解,但由于复杂的原因,它不保留底层函数的签名,所以任何用 @functools.lru_cache
装饰的函数都会被移除所有类型注解。import functools
@functools.lru_cache
def add_one(x: float) -> float:
return x + 1
add_one("Hello, world!")
typing
模块。我通常在跟候选人作广泛的技术讨论时,会展示一个使用了typing.Protocol
的代码片段,我不记得有任何候选人看到过这个特定的构造——当然,这完全没问题!但这体现了 typing 在 Python 生态的流行程度。if condition:
value: str = "Hello, world"
else:
# Not ok -- we declared `value` as `str`, and this is `None`!
value = None
...
if condition:
value: str = "Hello, world"
else:
# Not ok -- we already declared the type of `value`.
value: Optional[str] = None
...
# This is ok!
if condition:
value: Optional[str] = "Hello, world"
else:
value = None
from typing import Literal
def my_func(value: Literal['a', 'b']) -> None:
...
for value in ('a', 'b'):
# Not ok -- `value` is `str`, not `Literal['a', 'b']`.
my_func(value)
@overload
,来自typing
模块:非常强大,但很难正确使用。当然,如果需要重载一个方法,我就会使用它——但是,就像我说的,如果可以的话,我宁可避免它。@overload
def clean(s: str) -> str:
...
@overload
def clean(s: None) -> None:
...
def clean(s: Optional[str]) -> Optional[str]:
if s:
return s.strip().replace("\u00a0", " ")
else:
return None
@overload
def lookup(
paths: Iterable[str], *, strict: Literal[False]
) -> Mapping[str, Optional[str]]:
...
@overload
def lookup(
paths: Iterable[str], *, strict: Literal[True]
) -> Mapping[str, str]:
...
@overload
def lookup(
paths: Iterable[str]
) -> Mapping[str, Optional[str]]:
...
def lookup(
paths: Iterable[str], *, strict: Literal[True, False] = False
) -> Any:
pass
bool
到 find_many_latest
,你必须传一个字面量 True
或False
。@typing.overload
或者@overload
、在类方法中使用@overload
,等等。TypedDict
,同样来自typing
模块:可能很有用,但往往会产生笨拙的代码。TypedDict
——它必须用字面量 key 构造——所以下方第二种写法是行不通的:from typing import TypedDict
class Point(TypedDict):
x: float
y: float
a: Point = {"x": 1, "y": 2}
# error: Expected TypedDict key to be string literal
b: Point = {**a, "y": 3}
TypedDict
对象做一些 Pythonic 的事情。我最终倾向于使用 dataclass
或 typing.NamedTuple
对象。F = TypeVar("F", bound=Callable[..., Any])
def decorator(func: F) -> F:
def wrapper(*args: Any, **kwargs: Any):
return func(*args, **kwargs)
return cast(F, wrapper)
@decorator
def f(a: int) -> str:
return str(a)
@functools.lru_cache
:由于装饰器最终定义了一个全新的函数,所以如果你不正确地注解代码,就可能会出现严重而令人惊讶的错误。)ParamSpec
对装饰器的情况作了重大的改进。)reveal_type
*,*可以让 Mypy 在对文件进行类型检查时,显示出变量的推断类型。这是非常非常非常有用的。# No need to import anything. Just call `reveal_type`.
# Your editor will flag it as an undefined reference -- just ignore that.
x = 1
reveal_type(x) # Revealed type is "builtins.int"
reveal_type
特别地有用,因为它可以帮助你理解泛型是如何被“填充”的、类型是否被缩小了,等等。Any
毒害,我们在一组文件上写了调用 Mypy 的单元测试,并断言 Mypy 抛出的错误能匹配一系列预期内的异常:def test_check_function(self) -> None:
result = api.run(
[
os.path.join(
os.path.dirname(__file__),
"type_check_examples/function.py",
),
"--no-incremental",
],
)
actual = result[0].splitlines()
expected = [
# fmt: off
'type_check_examples/function.py:14: error: Incompatible return value type (got "str", expected "int")', # noqa: E501
'type_check_examples/function.py:19: error: Missing positional argument "x" in call to "__call__" of "FunctionPipeline"', # noqa: E501
'type_check_examples/function.py:22: error: Argument "x" to "__call__" of "FunctionPipeline" has incompatible type "str"; expected "int"', # noqa: E501
'type_check_examples/function.py:25: note: Revealed type is "builtins.int"', # noqa: E501
'type_check_examples/function.py:28: note: Revealed type is "builtins.int"', # noqa: E501
'type_check_examples/function.py:34: error: Unexpected keyword argument "notify_on" for "options" of "Expression"', # noqa: E501
'pipeline.py:307: note: "options" of "Expression" defined here', # noqa: E501
"Found 4 errors in 1 file (checked 1 source file)",
# fmt: on
]
self.assertEqual(actual, expected)
typing
模块在每个 Python 版本中都有很多改进,同时,还有一些特性会通过typing-extensions
模块向后移植。typing-extensions
,在前面提到的工作流编排库中使用了3.10 版本的ParamSpec
。(遗憾的是,PyCharm 似乎不支持通过typing-extensions
引入的ParamSpec
语法,并将其标记为一个错误,但是,还算好吧。)当然,Python 本身语法变化而出现的特性,不能通过typing-extensions
获得。typing
模块中有很多有用的辅助对象,NewType
是我的最爱之一。NewType
可让你创建出不同于现有类型的类型。例如,你可以使用NewType
来定义合规的谷歌云存储 URL,而不仅是str
类型,比如:from typing import NewType
GCSUrl = NewType("GCSUrl", str)
def download_blob(url: GCSUrl) -> None:
...
# Incompatible type "str"; expected "GCSUrl"
download_blob("gs://my_bucket/foo/bar/baz.jpg")
# Ok!
download_blob(GCSUrl("gs://my_bucket/foo/bar/baz.jpg"))
download_blob
的调用者指出它的意图,我们使这个函数具备了自描述能力。NewType
对于将原始类型(如 str
和 int
)转换为语义上有意义的类型特别有用。mypy
,冷缓存大约需要 50-60 秒,热缓存大约需要 1-2 秒。typing
模块、注解语法和 Mypy 本身的显著改进。(例如:新的联合类型语法( X|Y
)、 ParamSpec
和 TypeAlias
,这些都包含在 Python 3.10 中。)