
深入 mypy 进阶类型系统NoReturn、NewType、函数重载、self 类型与 async 代码的类型标注实战【免费下载链接】mypyOptional static typing for Python项目地址: https://gitcode.com/GitHub_Trending/my/mypy导读本篇文章基于 mypy 官方文档《More types》对应仓库 docs/source/more_types.rst展开系统讲解几类“按需使用”的进阶类型工具NoReturn永不返回的函数、NewType运行时零开销的新类型、overload函数重载、self 类型的高级用法以及async/await协程与异步迭代器的类型标注。读完本文你将掌握这些类型在 mypy 中的完整语义、约束与坑点并能在自己的代码库中写出更精确、更安全的类型标注。这些特性都属于“场景化”工具不是每个项目都用到但一旦遇到例如需要表达参数与返回值之间的关联、区分同构的数值 ID、给异步代码加类型它们就是不可替代的。建议先通读一遍之后在真正需要时再回来查阅对应小节。本文快速导航NoReturn告诉 mypy 某个函数永不正常返回例如无条件抛异常并让调用点之后的代码被识别为不可达NewType定义一个在 mypy 眼中与原类型完全不同的新类型但运行时与原类型完全一致如UserId之于intoverload 函数重载为一个函数声明多个签名精确编码参数与返回值之间的关联关系self 类型的高级用法在泛型类中限制方法的可用类型参数、为 mixin 提供宿主协议、精确标注替代构造器async 类型让 mypy 对async/await程序进行类型检查包括异步迭代器与异步生成器的正确标注。NoReturn声明“永不返回”的函数基本用法Mypy 提供了对永不返回函数的支持。典型例子是无条件抛出异常的函数from typing import NoReturn def stop() - NoReturn: raise Exception(no way)Mypy 会确保标注为NoReturn的函数真正不会返回——无论是以隐式自然走到函数末尾还是显式return语句的方式。如果函数体末尾存在未抛异常的执行路径mypy 会报错。不可达代码识别更重要的收益是mypy 会识别出调用此类函数之后的所有代码是不可达的并据此放宽检查。例如def f(x: int) - int: if x 0: return x stop() return whatever works # No error in an unreachable block这里stop()的返回类型是NoReturn因此函数永远不会执行到return whatever works这一行mypy 不再对该不可达代码块中的类型错误str不是int报错。从源码结构看这一语义在 mypy 的类型检查核心 mypy/checker.py 中实现检查器在分析调用表达式时识别NoReturn返回类型将后续语句标记为不可达mypy/checker.py 附近注释 “This is a NoReturn function”。NoReturn被 mypy 视为一个特殊的内置类型其合法来源包括typing.NoReturn、typing_extensions.NoReturn与mypy_extensions.NoReturn参见 mypy/types.py。版本兼容与安装在较老的 Python 版本3.6.2 之前中NoReturn尚未进入标准库typing模块。此时需要使用 pip 安装typing_extensions才能在你的代码中使用NoReturn。Python 3 命令行安装方式python3 -m pip install --upgrade typing-extensions安装后可以from typing_extensions import NoReturn使用在支持typing.NoReturn的 Python 版本上则直接from typing import NoReturn。NewType零运行时开销的“新类型”动机为什么需要区分同构类型有些场景下我们希望避免把不同类型的值混用导致的编程错误——例如用户 ID 和内部序号虽然都是整数但语义完全不同。直观做法是定义简单的派生类来区分class UserId(int): pass def get_by_user_id(user_id: UserId): ...但这引入了运行时开销创建真正的子类、__init__调用等。为了在几乎零运行时开销的前提下获得类型安全typing模块提供了辅助对象NewType。NewType 的语义Mypy 会把语句Derived NewType(Derived, Base)视作大致等价于如下类定义class Derived(Base): def __init__(self, _x: Base) - None: ...但在运行时NewType(Derived, Base)返回的是一个简单的可调用对象它只是原样返回传入的参数def Derived(_x): return _x也就是说类型检查层面它是一个全新的类型运行层面它与基类完全一致没有额外开销。类型兼容规则与示例Mypy 对NewType的兼容规则是在期望UserId的位置传入int需要显式转换即必须调用UserId(...)在期望int的位置传入UserId隐式兼容因为UserId本质是int的变体。from typing import NewType UserId NewType(UserId, int) def name_by_id(user_id: UserId) - str: ... UserId(user) # Fails type check name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK num: int UserId(5) 1上面num: int UserId(5) 1之所以成立是因为UserId是int的变体可以与int直接参与算术运算且结果仍是int。构造约束NewType严格接受两个参数第一个参数必须是字符串字面量内容为新类型的名称且必须与接收新类型的变量名一致第二个参数必须是一个可以被正确继承的类——不能是类型构造器例如 联合类型int | str、Optional等。NewType返回的可调用对象只接受一个参数这等价于只支持一个“接受一个基类实例”的构造函数。示例from typing import NewType class PacketId: def __init__(self, major: int, minor: int) - None: self._major major self._minor minor TcpPacketId NewType(TcpPacketId, PacketId) packet PacketId(100, 100) tcp_packet TcpPacketId(packet) # OK tcp_packet TcpPacketId(127, 0) # Fails in type checker and at runtime此外不能对NewType返回的对象调用isinstance或issubclass也不能继承subclass它——因为运行时它只是一个函数。与类型别名Type Alias的本质区别重要提醒与类型别名不同NewType会创建一个全新的、独一无二的类型。它的设计初衷就是帮助你发现“不小心把基类与新派生类型混用”的场景。使用类型别名时UserId与int是同义词下面代码能通过检查UserId int def name_by_id(user_id: UserId) - str: ... name_by_id(3) # ints and UserId are synonymous而换成NewType后同样的调用无法通过检查from typing import NewType UserId NewType(UserId, int) def name_by_id(user_id: UserId) - str: ... name_by_id(3) # int is not the same as UserId选择建议如果只是想让代码更可读、不关心强区分用类型别名即可如果希望强制区分两类“看起来一样”的值用NewType。函数重载用 overload 精确描述参数与返回值的关联动机联合类型不够用有些函数的参数与返回类型之间存在相互依赖关系仅靠 联合类型 无法表达。假设我们要写一个处理鼠标事件的函数传入一个坐标时返回ClickEvent传入两个坐标时返回DragEvent。第一版“朴素”写法def mouse_event(x1: int, y1: int, x2: int | None None, y2: int | None None) - ClickEvent | DragEvent: if x2 is None and y2 is None: return ClickEvent(x1, y1) elif x2 is not None and y2 is not None: return DragEvent(x1, y1, x2, y2) else: raise TypeError(Bad arguments)这个签名虽然能用但太宽松了它暗示无论传入几个参数mouse_event都可能返回ClickEvent或DragEvent中的任意一个它无法禁止调用方传入错误数量的整数——mypy 会把mouse_event(1, 2, 20)当作合法调用。更好的方案是使用 PEP 484 的函数重载为同一个函数给出多个类型标注签名更精确地描述行为from typing import overload # Overload *variants* for mouse_event. # These variants give extra information to the type checker. # They are ignored at runtime. overload def mouse_event(x1: int, y1: int) - ClickEvent: ... overload def mouse_event(x1: int, y1: int, x2: int, y2: int) - DragEvent: ... # The actual *implementation* of mouse_event. # The implementation contains the actual runtime logic. # # It may or may not have type hints. If it does, mypy # will check the body of the implementation against the # type hints. # # Mypy will also check and make sure the signature is # consistent with the provided variants. def mouse_event(x1: int, y1: int, x2: int | None None, y2: int | None None) - ClickEvent | DragEvent: if x2 is None and y2 is None: return ClickEvent(x1, y1) elif x2 is not None and y2 is not None: return DragEvent(x1, y1, x2, y2) else: raise TypeError(Bad arguments)现在 mypy 能精确理解调用mouse_event(5, 25)的返回类型恒为ClickEventmouse_event(5, 25, 2)会被报告为错误不匹配任何变体。重载的运行时行为一个重载函数必须由两个或更多重载变体overload variants后跟一个实现implementation组成且它们必须在代码中相邻——可以把它们视为一个不可分割的整体。关键规则变体函数体必须为空使用...或pass只有实现允许包含代码。因为运行时变体完全被忽略它们会被最后的实现函数覆盖。因此重载函数仍然是普通的 Python 函数不存在自动分发机制你必须自己在实现中手动处理不同类型例如用if语句和isinstance检查。如果是在stub 文件.pyi中添加重载则省略实现函数——stub 不包含运行时逻辑。约定虽然可以用pass关键字让变体函数体为空但更常见的惯例是使用省略号字面量...。mypy 如何检查对重载函数的调用调用重载函数时mypy 会综合考虑参数类型与参数个数arity选择最佳匹配的变体来推断返回类型。注意调用永远不会对照实现签名来检查——这正是mouse_event(5, 25, 3)虽然匹配实现签名却被报错的原因。平局规则“选择第一个匹配”当存在多个同样好的匹配变体时mypy 选择定义在最前面的那个。例如# For Python 3.8 and below you must use typing.List instead of list. e.g. # from typing import List from typing import overload overload def summarize(data: list[int]) - float: ... overload def summarize(data: list[str]) - str: ... def summarize(data): if not data: return 0.0 elif isinstance(data[0], int): # Do int specific code else: # Do str-specific code # What is the type of output? float or str? output summarize([])summarize([])同时匹配两个变体空列表既可能是list[int]也可能是list[str]。此时 mypy 按“选择第一个匹配”规则平局output的推断类型为float。实现者有责任保证运行时也按同样的顺序打破平局。两个例外Any 与联合类型“选择第一个匹配”规则有两个例外例外一如果多个变体匹配是因为某个参数类型是Anymypy 会把推断结果也设为Anydynamic_var: Any some_dynamic_function() # output2 is of type Any output2 summarize(dynamic_var)例外二如果多个变体匹配是因为一个或多个参数是联合类型mypy 会把推断结果设为所有匹配变体返回类型的联合some_list: list[int] | list[str] # output3 is of type float | str output3 summarize(some_list)实践建议由于“选择第一个匹配”规则的存在调整重载变体的顺序会改变 mypy 的类型检查结果。为把潜在问题降到最低建议让重载变体的顺序与实现中运行时检查如isinstance检查的顺序保持一致变体与运行时检查都按照从最具体到最不具体的顺序排列下文有示例。mypy 对变体的检查Mypy 会对重载变体定义执行多项检查确保它们按预期工作。检查一变体不能“遮蔽”后续变体考虑下面的函数把两个Expression对象相加其中有一个针对两个Literal类型的特例from typing import overload class Expression: # ...snip... class Literal(Expression): # ...snip... # Warning -- the first overload variant shadows the second! overload def add(left: Expression, right: Expression) - Expression: ... overload def add(left: Literal, right: Literal) - Literal: ... def add(left: Expression, right: Expression) - Expression: # ...snip...虽然这段代码技术上类型安全但它包含一个反模式第二个变体永远不会被选中调用add(Literal(3), Literal(4))时mypy 总是选第一个变体结果为Expression而非Literal。原因是Literal是Expression的子类型“选择第一个匹配”规则总是先看第一个变体就停住了。由于“永远无法被匹配到的重载变体”几乎必然是错误mypy 会报告错误。修复办法有两种1) 删除第二个重载2)交换两个重载的顺序# Everything is ok now -- the variants are correctly ordered # from most to least specific. overload def add(left: Literal, right: Literal) - Literal: ... overload def add(left: Expression, right: Expression) - Expression: ... def add(left: Expression, right: Expression) - Expression: # ...snip...检查二禁止“固有地不安全重叠”的变体Mypy 还会对变体进行类型检查标记固有地不安全重叠inherently unsafely overlapping的重载。看下面的不安全定义from typing import overload overload def unsafe_func(x: int) - int: ... overload def unsafe_func(x: object) - str: ... def unsafe_func(x: object) - int | str: if isinstance(x, int): return 42 else: return some string表面上这个函数没问题但实际使用时会在“推断类型”与“真实运行时类型”之间产生偏差some_obj: object 42 unsafe_func(some_obj) danger danger # Type checks, yet crashes at runtime!由于some_obj的类型是objectmypy 会判定unsafe_func返回str于是上面的表达式通过类型检查但运行时unsafe_func实际返回的是int导致崩溃mypy 会以尽力而为best-effort的方式检测并禁止这类不安全的重叠重载。两个变体被视为“不安全重叠”当且仅当同时满足第一个变体的所有参数都与第二个潜在兼容即第一个的参数类型是第二个参数类型的子类型或兼容类型第一个变体的返回类型不兼容于不是第二个变体返回类型的子类型。在上述例子中第一个变体的int参数是第二个变体object参数的子类型但返回类型int不是str的子类型——两个条件都成立mypy 会正确标记unsafe_func为不安全。注意即使你忽略了重叠错误例如用# type: ignore或--disable-error-codemypy 通常仍会在调用点推断出你期望的类型。检查的边界为什么 mypy 不拦截所有不安全用法Mypy 无法检测所有不安全的重载用法。例如把上面的代码换成调用summarizesome_list: list[str] [] summarize(some_list) danger danger # Type safe, yet crashes at runtime!只看重载的注解这个程序能通过类型检查但由于summarize设计上在收到空列表时偏向返回 float程序运行时会崩溃。mypy 不把summarize这类定义标记为潜在不安全原因是如果这样做将极难写出安全的重载。例如假设一个重载有两个变体分别接受类型A和B——即使A与B完全无关用户仍可能传入一个同时继承A和B的第三种类型C的值从而触发类似上面的运行时错误。好消息是这类情况相对少见。它真正的含义是在设计和调用可能接收到“同时是两种看似无关类型的实例”的重载函数时要格外小心。mypy 对实现的检查实现的函数体会对照实现上的类型提示进行类型检查。以MyList为例函数体以参数列表index: int | slice和返回类型T | Sequence[T]进行校验。如果实现上没有类型注解则函数体不进行类型检查若想强制检查使用--check-untyped-defs标志详见 未标注定义与调用 与 命令行选项 中的说明。变体也必须与实现的类型提示兼容。在MyList示例中mypy 会检查第一个变体的参数类型int和返回类型T与int | slice、T | Sequence兼容对第二个变体验证参数slice与返回Sequence[T]兼容。实用示例为自定义容器重载getitem假设要实现一个自定义容器类实现__getitem__[]下标访问。如果传入整数应返回单个元素传入slice应返回元素序列。可以用重载精确编码参数与返回类型的关系Python 3.12 语法from collections.abc import Sequence from typing import overload class MyListT: overload def __getitem__(self, index: int) - T: ... overload def __getitem__(self, index: slice) - Sequence[T]: ... def __getitem__(self, index: int | slice) - T | Sequence[T]: if isinstance(index, int): # Return a T here ... elif isinstance(index, slice): # Return a sequence of Ts here ... else: raise TypeError(...)同一示例的旧式语法Python 3.11 及更早from collections.abc import Sequence from typing import TypeVar, overload T TypeVar(T) class MyList(Sequence[T]): overload def __getitem__(self, index: int) - T: ... overload def __getitem__(self, index: slice) - Sequence[T]: ... def __getitem__(self, index: int | slice) - T | Sequence[T]: if isinstance(index, int): # Return a T here ... elif isinstance(index, slice): # Return a sequence of Ts here ... else: raise TypeError(...)提示如果只是想把类型变量约束到某些类型或其子类型可以用 值约束类型变量value-constrained type variables不必动用重载。用 ... 代替默认值以减少冗余函数参数的默认值本身不影响签名——只有“有没有默认值”才影响签名。因此可以在重载定义中用...占位代替默认值减少冗余from typing import overload class M: ... overload def get_model(model_or_pk: M, flag: bool ...) - M: ... overload def get_model(model_or_pk: int, flag: bool ...) - M | None: ... def get_model(model_or_pk: int | M, flag: bool True) - M | None: ...重载在 mypy 内部的实现线索从源码结构看重载在 mypy 中对应OverloadedFuncDef节点语义分析阶段由 mypy/semanal.py 的analyze_overloaded_func_def处理它会找出所有重载签名、实现以及缺少overload装饰器的项mypy/semanal.py 附近的find_overload_signatures逻辑。类型层面重载函数对应Overloaded类型继承自FunctionLike见 mypy/types.py每个变体是CallableTypemypy/types.py。成员访问层面如重载的属性/方法的选择逻辑可以参考 mypy/checkmember.py 中“对重载属性选择第一个通过 self 参数检查的项”的注释。历史语义变化mypy 0.620 起上述重载语义是mypy 0.620 起的新行为此前 mypy 会对所有重载变体做类型擦除type erasure。例如上面summarize的例子曾经是非法的因为list[str]与list[int]都被擦除为list[Any]。这一限制在 mypy 0.620 中移除。此前 mypy 用另一套算法选择最佳匹配变体若匹配失败默认返回Any。新算法采用“选择第一个匹配”规则且仅当输入参数本身包含Any时才回退到Any。条件重载Conditional overloads有时按条件定义重载很有用常见场景包括类型在运行时不可用或只在某个 Python 版本存在。所有既有的重载规则依然适用例如至少要有两个重载。限制mypy 只能推断有限的条件。当前支持的条件包括typing.TYPE_CHECKING、MYPY、版本与平台检查、以及 --always-true / --always-false 指定的值。示例一基于TYPE_CHECKING的条件重载from typing import TYPE_CHECKING, Any, overload if TYPE_CHECKING: class A: ... class B: ... if TYPE_CHECKING: overload def func(var: A) - A: ... overload def func(var: B) - B: ... def func(var: Any) - Any: return var reveal_type(func(A())) # Revealed type is A示例二基于 Python 版本的条件重载# flags: --python-version 3.10 import sys from typing import Any, overload class A: ... class B: ... class C: ... class D: ... if sys.version_info (3, 7): overload def func(var: A) - A: ... elif sys.version_info (3, 10): overload def func(var: B) - B: ... else: overload def func(var: C) - C: ... overload def func(var: D) - D: ... def func(var: Any) - Any: return var reveal_type(func(B())) # Revealed type is B reveal_type(func(C())) # No overload variant of func matches argument type C # Possible overload variants: # def func(var: B) - B # def func(var: D) - D # Revealed type is Any说明最后一个示例中mypy 以 --python-version 3.10 执行因此条件sys.version_info (3, 10)成立B的重载被加入A与C的重载被忽略D的重载非条件定义也会被加入。当 mypy无法推断某个条件恒为True或恒为False时会发出错误from typing import Any, overload class A: ... class B: ... def g(bool_var: bool) - None: if bool_var: # Condition cant be inferred, unable to merge overloads overload def func(var: A) - A: ... overload def func(var: B) - B: ... def func(var: Any) - Any: ... reveal_type(func(A())) # Revealed type is Anyself 类型的高级用法通常情况下mypy 不要求为实例方法与类方法的第一个参数self/cls标注类型。但某些编程模式需要更精确的静态类型此时可以显式标注 self 类型。泛型类中的受限方法Restricted methods在泛型类中某些方法可能只允许在特定类型参数值下调用Python 3.12 语法class Tag[T]: item: T def uppercase_item(self: Tag[str]) - str: return self.item.upper() def label(ti: Tag[int], ts: Tag[str]) - None: ti.uppercase_item() # E: Invalid self argument Tag[int] to attribute function # uppercase_item with type Callable[[Tag[str]], str] ts.uppercase_item() # This is OK该模式还支持在类型参数本身是泛型时匹配嵌套类型Python 3.12 语法from collections.abc import Sequence class Storage[T]: def __init__(self, content: T) - None: self._content content def first_chunkS - S: return self._content[0] page: Storage[list[str]] page.first_chunk() # OK, type is str Storage(0).first_chunk() # Error: Invalid self argument Storage[int] to attribute function # first_chunk with type Callable[[Storage[Sequence[S]]], S]最后可以在 self 类型上使用重载来表达某些棘手方法的精确类型Python 3.12 语法from collections.abc import Callable from typing import overload class Tag[T]: overload def export(self: Tag[str]) - str: ... overload def export(self, converter: Callable[[T], str]) - str: ... def export(self, converterNone): if isinstance(self.item, str): return self.item return converter(self.item)特别地基于 self 类型重载__init__可能对标注泛型类的构造函数很有用——当类型参数以非平凡方式依赖构造参数时例如标准库中的 subprocess.Popen。Mixin 类用宿主类协议作为 self 类型在 mixin 方法中使用宿主类协议作为 self 类型可以提升 mixin 静态类型的代码复用性。例如定义一个描述宿主类公共功能的协议而不是给每个 mixin 添加必需的抽象方法class Lockable(Protocol): property def lock(self) - Lock: ... class AtomicCloseMixin: def atomic_close(self: Lockable) - int: with self.lock: # perform actions ... class AtomicOpenMixin: def atomic_open(self: Lockable) - int: with self.lock: # perform actions ... class File(AtomicCloseMixin, AtomicOpenMixin): def __init__(self) - None: self.lock Lock() class Bad(AtomicCloseMixin): pass f File() b: Bad f.atomic_close() # OK b.atomic_close() # Error: Invalid self type for atomic_close注意当显式 self 类型不是当前类的超类型时它必须是一个协议。此时 mypy 只在调用点检查 self 类型的有效性。替代构造器的精确类型标注有些类定义了替代构造器alternative constructors。当这些类是泛型时self 类型可以给它们精确的签名Python 3.12 语法使用typing.Selffrom typing import Self class Base[T]: def __init__(self, item: T) - None: self.item item classmethod def make_pair(cls, item: T) - tuple[Self, Self]: return cls(item), cls(item) class SubT: ... pair Sub.make_pair(yes) # Type is tuple[Sub[str], Sub[str]] bad Sub[int].make_pair(no) # Error: Argument 1 to make_pair of Base # has incompatible type str; expected int这里make_pair返回tuple[Self, Self]因此对Sub.make_pair(yes)推断出tuple[Sub[str], Sub[str]]保留了具体子类的精确类型。类型化 async/await 代码Mypy 支持对使用async/await语法的协程进行类型检查。协程的更多背景可参考 PEP 492 与 Python 官方 asyncio 文档。基本规则用async def定义的函数与普通函数的类型化方式类似。返回类型注解应该写成你await这个协程后期望拿到的值的类型import asyncio async def format_string(tag: str, count: int) - str: return fT-minus {count} ({tag}) async def countdown(tag: str, count: int) - str: while count 0: my_str await format_string(tag, count) # type is inferred to be str print(my_str) await asyncio.sleep(0.1) count - 1 return Blastoff! asyncio.run(countdown(Millennium Falcon, 5))不 await 直接调用async def函数的结果会被自动推断为类型Coroutine[Any, Any, T]它是Awaitable[T]的子类型my_coroutine countdown(Millennium Falcon, 5) reveal_type(my_coroutine) # Revealed type is typing.Coroutine[Any, Any, builtins.str]异步迭代器Async iterators如果有异步迭代器可以用collections.abc.AsyncIterator类型进行标注from collections.abc import AsyncIterator from typing import Optional import asyncio class arange: def __init__(self, start: int, stop: int, step: int) - None: self.start start self.stop stop self.step step self.count start - step def __aiter__(self) - AsyncIterator[int]: return self async def __anext__(self) - int: self.count self.step if self.count self.stop: raise StopAsyncIteration else: return self.count async def run_countdown(tag: str, countdown: AsyncIterator[int]) - str: async for i in countdown: print(fT-minus {i} ({tag})) await asyncio.sleep(0.1) return Blastoff! asyncio.run(run_countdown(Serenity, arange(5, 0, -1)))注意__anext__返回int因为async for中每个i就是int迭代结束通过StopAsyncIteration表达。异步生成器Async generators异步生成器PEP 525 引入是创建异步迭代器的简便方式from collections.abc import AsyncGenerator from typing import Optional import asyncio # Could also type this as returning AsyncIterator[int] async def arange(start: int, stop: int, step: int) - AsyncGenerator[int, None]: current start while (step 0 and current stop) or (step 0 and current stop): yield current current step asyncio.run(run_countdown(Battlestar Galactica, arange(5, 0, -1)))注释说明这里也可以标注为AsyncIterator[int]。常见困惑yield 对 async def 类型的影响一个常见的混淆点是async def函数中是否存在yield语句会改变函数的类型from collections.abc import AsyncIterator async def arange(stop: int) - AsyncIterator[int]: # When called, arange gives you an async iterator # Equivalent to Callable[[int], AsyncIterator[int]] i 0 while i stop: yield i i 1 async def coroutine(stop: int) - AsyncIterator[int]: # When called, coroutine gives you something you can await to get an async iterator # Equivalent to Callable[[int], Coroutine[Any, Any, AsyncIterator[int]]] return arange(stop) async def main() - None: reveal_type(arange(5)) # Revealed type is typing.AsyncIterator[builtins.int] reveal_type(coroutine(5)) # Revealed type is typing.Coroutine[Any, Any, typing.AsyncIterator[builtins.int]] await arange(5) # Error: Incompatible types in await (actual type AsyncIterator[int], expected type Awaitable[Any]) reveal_type(await coroutine(5)) # Revealed type is typing.AsyncIterator[builtins.int]含yield的async def异步生成器被调用后返回异步迭代器本身不含yield的async def协程被调用后返回协程对象需要先await才能拿到异步迭代器。这一差异有时会在定义基类、Protocol 或重载时带来问题from collections.abc import AsyncIterator from typing import Protocol, overload class LauncherIncorrect(Protocol): # Because launch does not have yield, this has type # Callable[[], Coroutine[Any, Any, AsyncIterator[int]]] # instead of # Callable[[], AsyncIterator[int]] async def launch(self) - AsyncIterator[int]: raise NotImplementedError class LauncherCorrect(Protocol): def launch(self) - AsyncIterator[int]: raise NotImplementedError class LauncherAlsoCorrect(Protocol): async def launch(self) - AsyncIterator[int]: raise NotImplementedError if False: yield 0 # The type of the overloads is independent of the implementation. # In particular, their type is not affected by whether or not the # implementation contains a yield. # Use of def makes it clear the type is Callable[..., AsyncIterator[int]], # whereas with async def it would be Callable[..., Coroutine[Any, Any, AsyncIterator[int]]] overload def launch(*, count: int ...) - AsyncIterator[int]: ... overload def launch(*, time: float ...) - AsyncIterator[int]: ... async def launch(*, count: int 0, time: float 0) - AsyncIterator[int]: # The implementation of launch is an async generator and contains a yield yield 0要点总结LauncherIncorrect中async def launch没有yield其类型是Callable[[], Coroutine[Any, Any, AsyncIterator[int]]]不是预期的Callable[[], AsyncIterator[int]]与协议不匹配修正方式有二改用普通def launchLauncherCorrect或保留async def但让代码路径中存在yieldLauncherAlsoCorrect重载变体的类型与实现无关——尤其不受实现中是否含yield影响。因此用def明确写出Callable[..., AsyncIterator[int]]会比async def隐式的Coroutine[Any, Any, AsyncIterator[int]]更清晰。总结何时使用这些进阶类型特性核心用途关键约束NoReturn标注永不返回的函数激活不可达代码识别函数体必须真的永不返回NewType以零运行时开销区分同构类型两参数首参为字符串字面量不能 isinstance/issubclass/继承overload编码参数与返回值的关联精确调用检查≥2 个空体变体 相邻实现按“第一个匹配”选型小心遮蔽与不安全重叠self 类型泛型类受限方法、mixin 宿主协议、替代构造器非超类型时必须为 Protocol在调用点校验async 类型协程、异步迭代器、异步生成器的精确标注返回注解写 await 后值类型yield 决定 async def 的调用类型mypy 的重载与类型检查引擎mypy/checker.py、mypy/semanal.py、mypy/types.py为上述所有语义提供了底层支撑。本文内容完全继承自 mypy 官方文档 docs/source/more_types.rst并与 kinds_of_types.rst联合类型等基础类型、generics.rst值约束类型变量、type_inference_and_annotations.rst未标注定义与--check-untyped-defs以及 command_line.rst--always-true/--always-false/--python-version等章节相互衔接。阅读完本文后建议进一步阅读这些文档构建完整的 mypy 类型体系知识。【免费下载链接】mypyOptional static typing for Python项目地址: https://gitcode.com/GitHub_Trending/my/mypy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考