尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

mypy 类型种类详解:从类类型、Any 到联合、Optional、别名与生成器的完整实践指南

mypy 类型种类详解:从类类型、Any 到联合、Optional、别名与生成器的完整实践指南 mypy 类型种类详解从类类型、Any 到联合、Optional、别名与生成器的完整实践指南【免费下载链接】mypyOptional static typing for Python项目地址: https://gitcode.com/GitHub_Trending/my/mypy导读mypy 是 Python 的可选静态类型检查器Optional static typing在掌握内置类型之后任何非平凡程序都离不开若干高级类型形式。本文以官方文档 kinds_of_types.rst 为主体系统讲解类类型、Any、元组类型、Callable、联合类型int | str、Optional/None类型、类型别名、命名元组、type[C]类对象类型与生成器类型并结合仓库源码如 types.py、semanal.py揭示其底层实现原理。读完本文你将能准确选择并书写这十类类型标注写出类型安全、可维护的 Python 代码。类类型Class types一切从类开始每个类本身都是一个合法的类型。子类的任何实例都与所有父类兼容因此可以推导出每个值都与object类型兼容也与下文讨论的Any兼容。mypy 会分析类的函数体来确定实例上可用哪些方法与属性。下面的例子展示了继承场景class A: def f(self) - int: # self 的类型被推断为 A return 2 class B(A): def f(self) - int: return 3 def g(self) - int: return 4 def foo(a: A) - None: print(a.f()) # 3 a.g() # Error: A has no attribute g foo(B()) # OKB 是 A 的子类关键点在于mypy 按声明的静态类型检查属性访问而非运行时实际对象的类型。foo中形参声明为A因此即便传入的是B()实例调用a.g()依然报错——因为静态类型A上没有定义g。这与鸭子类型不同属于结构化类型检查之外的**名义类型nominal typing**行为。Any 类型动态类型的安全出口Any 是什么Any类型的值是动态类型的。mypy 对该值可能的运行时类型一无所知允许对其执行任何操作并且这些操作只在运行时才被检查。当由于某种原因无法使用更精确的类型时可以把Any当作逃生舱escape hatch使用。这一点不要与object混淆object代表所有值的集合这一静态类型而Any引入了类型不安全。两者差异详见 dynamic_typing.rst 的 Any vs. object 小节object值只允许执行对所有类型都合法的操作如print(o)、isinstance(o, int)、赋值给o而调用o.foo()、o 2、open(o)都会报错。如果拿不准该用object还是Any优先用object只有收到类型检查器报错时才切换到Any。Any 与所有类型双向兼容Any与每个其他类型互相兼容既可以把Any值赋给更精确类型的变量也可以反过来a: Any None s: str a 2 # OK把 int 赋给 Any s a # OK把 Any 赋给 str声明的以及推断的类型在运行时会被忽略或者说擦除。它们基本被当作注释处理因此上面的代码在运行时不会产生错误——尽管运行时s实际拿到的是int值而其声明类型是str使用Any时要格外小心它让你可以向 mypy 撒谎从而轻易隐藏 bug。未标注参数与返回值默认是 Any如果没有为函数参数或返回值定义类型它们默认就是Anydef show_heading(s) - None: print( s ) # 无静态检查因为 s 是 Any show_heading(1) # OK只在运行时报错mypy 不会报错即使函数不返回任何值也应当为静态类型函数显式标注None返回类型这样 mypy 才能捕获额外的类型错误def wait(t: float): # 隐式 Any 返回值 print(Waiting...) time.sleep(t) if wait(2) 1: # Mypy 捕获不到这个错误 ...改为显式None返回类型后错误立刻暴露def wait(t: float) - None: print(Waiting...) time.sleep(t) if wait(2) 1: # Error: cant compare None and int ...注意签名中没有任何类型的函数是动态类型的。动态类型函数的函数体不会进行静态检查局部变量具有隐式Any类型。这让存量 Python 代码迁移到 mypy 更加容易因为 mypy 不会对动态类型函数提出抱怨。源码视角Any 的九种来源在仓库源码 mypy/types.py 中TypeOfAny类把Any细分为九种来源用于报告与精确诊断来源含义unannotated无类型标注时推断出的 Anyexplicit来自显式类型标注from_unimported_type来自未跟随导入配合--disallow-any-unimportedfrom_omitted_generics来自省略的泛型参数如裸listfrom_error来自某个错误special_form无法在 mypy 类型系统中表示的类型如NewType(...)的调用from_another_any来自与另一个 Any 交互implementation_artifact来自实现限制/缺陷suggestion_engine建议引擎插入的 Any同时mypy/options.py 中默认关闭的--disallow-untyped-defs、--disallow-any-generics等选项可以让你对隐式 Any保持警惕。例如--disallow-any-generics会把裸list这种省略泛型参数的写法当作错误。更多细节见 dynamic_typing.rst文中还指出从 Any 值派生出的值通常也是 Any且 Any 会在程序中传播使类型检查效果下降。元组类型Tuple types类型tuple[T1, ..., Tn]表示一个元素类型依次为T1、…、Tn的元组Python 3.8 及更早版本请用typing.Tupledef f(t: tuple[int, str]) - None: t 1, foo # OK t foo, 1 # 类型检查错误这种元组类型有精确的固定元素个数上例为 2 个。元组还可以用作不可变、长度可变的序列此时使用tuple[T, ...]这里的字面量...是语法的一部分def print_squared(t: tuple[int, ...]) - None: for n in t: print(n, n ** 2) print_squared(()) # OK print_squared((1, 3, 5)) # OK print_squared([1, 2]) # Error: 只有元组才合法提示通常用Sequence[T]比tuple[T, ...]更好因为collections.abc.Sequence也兼容 list 及其他非元组序列。提示tuple[...]从 Python 3.6 起可作为基类合法使用且在 stub 文件中始终合法。更早的 Python 版本可以退而求其次用命名元组作为基类见下文命名元组一节。Callable 类型可调用类型与 lambda在静态类型代码中可以传递函数对象和绑定方法。接受参数A1、…、An并返回Rt的函数类型写作Callable[[A1, ..., An], Rt]from collections.abc import Callable def twice(i: int, next: Callable[[int], int]) - int: return next(next(i)) def add(i: int) - int: return i 1 print(twice(3, add)) # 5注意Python 3.8 及更早版本请从typing导入Callable[...]而不是collections.abc。Callable 的限制与 Callable[..., T]在 Callable 类型中只能出现位置参数且不能有默认值——不过这已覆盖绝大多数使用场景。少数非常规场景下mypy 支持特殊形式Callable[..., T]字面量...它与任意可调用对象兼容只要其返回值与T兼容无论参数的数量、类型或种类如何。mypy 允许你用任意参数调用这种可调用值而不做任何检查——这方面它们被当作(*args: Any, **kwargs: Any)函数签名对待from collections.abc import Callable def arbitrary_call(f: Callable[..., int]) - int: return f(x) f(y2) # OK arbitrary_call(ord) # 无静态错误但运行时失败 arbitrary_call(open) # Error: 不返回 int arbitrary_call(1) # Error: int 不可调用需要更精确、更复杂的回调类型时可以使用灵活的回调协议callback protocols。lambda 同样受支持其参数与返回值类型无法显式给出只能通过双向类型推断bidirectional type inference依据上下文推断l map(lambda x: x 1, [1, 2, 3]) # 推断 x 为 intl 为 list[int]若想显式给出参数或返回值类型请改用普通可以嵌套的函数定义。Callable 与类对象Callable 还可以匹配类型对象type object即匹配其__init__或__new__签名from collections.abc import Callable class C: def __init__(self, app: str) - None: pass CallableType Callable[[str], C] def class_or_callable(arg: CallableType) - None: inst arg(my_app) reveal_type(inst) # Revealed type is C这在希望arg既可以是返回C实例的 Callable又可以是C本身时非常有用。这一机制对回调协议同样成立。联合类型Union typesPython 函数经常接受两种或更多不同类型的值。可以用函数重载overloading来表示但联合类型通常更方便。使用T1 | ... | Tn构造联合类型——例如参数类型为int | str时整数和字符串都是合法的实参值。可以用isinstance检查把联合类型收窄narrow为更具体的类型def f(x: int | str) - None: x 1 # Error: str int 不合法 if isinstance(x, int): # 这里 x 的类型是 int x 1 # OK else: # 这里 x 的类型是 str x a # OK f(1) # OK f(x) # OK f(1.1) # Error注意对联合类型执行的操作只有当它对每一个联合成员都合法时才被允许。这正是往往需要先用isinstance把联合收窄到非联合类型的原因。也正因如此建议避免把联合类型用作函数返回类型——调用方在使用返回值前可能不得不先做isinstance。旧语法与未来注解Python 3.9 及更早版本只部分支持|语法可以改用传统的Union[T1, ..., Tn]构造器from typing import Union def f(x: Union[int, str]) - None: ...在新语法不受运行时支持的 Python 版本上配合from __future__ import annotations也可以有限制地使用详见 runtime_troubles.rstfrom __future__ import annotations def f(x: int | str) - None: # Python 3.7 及以后可用 ...Optional 类型与 None 类型基本写法与 Optional[X]可以用T | None定义允许None值的类型变体例如int | None这就是所谓的optional 类型def strlen(s: str) - int | None: if not s: return None # OK return len(s) def strlen_invalid(s: str) - int: if not s: return None # Error: None 与 int 不兼容 return len(s)也可以使用typing.Optional修饰符如Optional[int]Optional[X]是Union[X, None]的简写from typing import Optional def strlen(s: str) - Optional[int]: ...未加防护的 None 操作不被允许大多数操作都不允许直接作用于未加防护的None或 optional 值上def my_inc(x: int | None) - int: return x 1 # Error: 不能对 None 和 int 做加法必须进行显式的None检查。mypy 具有强大的类型推断能力让你可以用常规 Python 惯用法防护None值例如识别is None检查def my_inc(x: int | None) - int: if x is None: return 0 else: # 这里 x 的推断类型就是 int return x 1因为 if 条件中检查了Nonemypy 会把 else 块中x的类型推断为int。其他受支持的防护检查还包括if x is not None、if x和if not x。此外mypy 还能理解逻辑表达式内的None检查def concat(x: str | None, y: str | None) - str | None: if x is not None and y is not None: # 这里 x 和 y 都不是 None return x y else: return None部分初始化状态与 assert x is not None有时 mypy 无法意识到某个值永远不会是None。典型场景是类实例可以存在于部分定义状态中——某个属性在对象构造期间初始化为None但某个方法假定该属性已不再是None。此时 mypy 会抱怨可能的None值可以用assert x is not None在方法中绕过class Resource: path: str | None None def initialize(self, path: str) - None: self.path path def read(self) - str: # 我们要求对象已经完成初始化 assert self.path is not None with open(self.path) as f: # OK return f.read() r Resource() r.initialize(/foo/bar) r.read()把变量初始化为None时None通常只是一个空占位值实际值有别的类型——这就是为什么上面的Resource类需要给属性加注解class Resource: path: str | None None ...该方法内定义的属性也同样适用class Counter: def __init__(self) - None: self.count: int | None None尽量不给初始值很多时候不为属性提供初始值反而更省事——这样就不需要使用 optional 类型也可以避免assert ... is not None检查。只要在类体中注解了属性就无需初始值class Container: items: list[str] # 没有初始值mypy 通常用变量第一次赋值来推断其类型。但如果同一作用域内同时赋了None值和非None值mypy 通常也能在没有注解的情况下做对def f(i: int) - None: n None # 由于下面的赋值推断类型为 int | None if i 0: n i ...有时你会看到 Cannot determine type of 错误。此时应添加显式的... | None注解。注意None是只有一个值即None的类型。None也用作不返回值即隐式返回None函数的返回类型。注意Python 解释器内部用NoneType作为None的类型名但类型注解中始终使用None——后者更短且更易读types.NoneType在 Python 3.10 可用更早版本完全不暴露。注意Optional[T]并不表示带默认值的函数参数。它只表示None是合法的实参值。这是一个常见误解——因为None是参数的常见默认值而带默认值的参数有时被称为optional参数。源码视角strict_optional 默认开启从 mypy/options.py 可以看到strict_optional默认值为True。在该模式下None不会被隐式当作任意类型的成员因此int | None这类显式 optional 注解才是表达可能为 None的标准方式。类型别名Type aliases某些情况下类型名可能又长又难写尤其是频繁使用时def f() - list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]]: ...此时可以简单地把类型赋给一个变量来定义类型别名这是隐式类型别名AliasType list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]] # 现在可以用 AliasType 代替完整写法 def f() - AliasType: ...注意类型别名不会创建新类型。它只是另一种类型的简写记号——除泛型别名generic aliases外它等价于目标类型。显式类型别名type 语句与 TypeAliasPython 3.12 引入了用于定义显式类型别名的type语句。显式别名没有歧义还能通过明确意图提高可读性type AliasType list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]] # 现在可以用 AliasType 代替完整写法 def f() - AliasType: ...隐式别名有一个容易混淆之处究竟什么时候赋值定义的是类型别名并不总是清晰的——例如别名包含前向引用、非法类型或违反类型别名声明的其他限制时。由于未注解变量与类型别名之间的区别是隐式的有歧义或不正确的类型别名声明默认会定义普通变量而不是类型别名。type语句定义的别名具有以下区别于隐式别名的特性定义中可以包含前向引用而无需字符串转义因为它是惰性求值的别名可以用在类型注解、类型实参和 cast 中但不能用在需要类对象的上下文中——例如不能作为基类也不能用来构造实例。还有更早的显式别名语法PEP 613from typing import TypeAlias AliasType: TypeAlias list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]]源码视角别名的语义分析在仓库的语义分析器 mypy/semanal.py 中TypeAliasStmt会经过递归别名检测对于无效/不支持的递归别名如元组项或联合项包含其自身、右侧类型变量嵌套发散mypy 会报错并把目标回退为TypeOfAny.from_error类型的Any。这印证了文档中有歧义或不正确的别名声明会退化为普通变量/错误处理的描述。语法层面type语句在 mypy/fastparse.py 中被解析为TypeAliasStmt或普通AssignmentStmt。命名元组Named tuplesmypy 能识别命名元组并对其定义/使用代码进行类型检查。下面的例子中可以检测到访问不存在的属性Point namedtuple(Point, [x, y]) p Point(x1, y2) print(p.z) # Error: Point 没有属性 z如果使用collections.namedtuple定义命名元组所有元素都会被假定为Any类型——即 mypy 对元素类型一无所知。可以使用typing.NamedTuple同时定义元素类型from typing import NamedTuple Point NamedTuple(Point, [(x, int), (y, int)]) p Point(x1, yx) # 参数类型不兼容str期望 intPython 3.6 引入了基于类的、带类型的命名元组替代语法from typing import NamedTuple class Point(NamedTuple): x: int y: int p Point(x1, yx) # 参数类型不兼容str期望 int注意在类型注解中可以使用原始的NamedTuple伪类表示任意 NamedTuple 对象都合法。例如它对反序列化很有用def deserialize_named_tuple(arg: NamedTuple) - Dict[str, Any]: return arg._asdict() Point namedtuple(Point, [x, y]) Person NamedTuple(Person, [(name, str), (age, int)]) deserialize_named_tuple(Point(x1, y2)) # ok deserialize_named_tuple(Person(nameNikita, age18)) # ok # Error: deserialize_named_tuple 的参数 1 类型不兼容 # Tuple[int, int]期望 NamedTuple deserialize_named_tuple((1, 2))注意该行为高度实验性、非标准其他类型检查器与 IDE 可能不支持。类对象的类型type[C]参考 PEP 484: The type of class objects 的自由改写。有时你想谈论继承自某个给定类的类对象。这可以写作type[C]Python 3.8 及更早版本用typing.Type[C]其中C是一个类。换句话说当C是类名时用C注解参数表示该参数是C或C子类的实例而用type[C]注解参数则表示该参数是一个派生自C的类对象或C本身。例如假设有如下类class User: # 定义 name、email 等字段 class BasicUser(User): def upgrade(self): 升级到 Pro class ProUser(User): def pay(self): 付费账单注意ProUser并不继承自BasicUser。下面这个函数会在你传入正确的类对象时创建其中某个类的实例def new_user(user_class): user user_class() # 这里可以把 user 对象写入数据库 return user如何注解这个函数如果无法参数化type最好的写法也只能是def new_user(user_class: type) - User: # 实现同上这看起来合理但在下面的例子中mypy 看不到buyer变量的类型是ProUserbuyer new_user(ProUser) buyer.pay() # 被拒绝不是 User 的方法然而使用type[C]语法配合带上限的类型变量type variable with an upper bound可以做得更好Python 3.12 语法def new_userU: User - U: # 实现同上使用传统语法的版本Python 3.11 及更早U TypeVar(U, boundUser) def new_user(user_class: type[U]) - U: # 实现同上现在当用User的具体子类调用new_user()时mypy 会推断出正确的结果类型beginner new_user(BasicUser) # 推断类型为 BasicUser beginner.upgrade() # OK注意type[C]对应的值必须是C的实际类对象子类型。其构造函数必须与C的构造函数兼容。如果C是类型变量其上限必须是一个类对象。生成器Generators只产出值的简单生成器可以简洁地注解返回类型为Iterator[YieldType]或Iterable[YieldType]。例如def squares(n: int) - Iterator[int]: for i in range(n): yield i * i一个经验法则是用尽可能具体的返回类型注解函数。但同时也要注意避免把实现细节泄漏到函数的公共 API。遵循这两条原则生成器函数应优先用Iterator[YieldType]而不是Iterable[YieldType]作为返回类型注解——它让 mypy 知道调用方可以对函数返回的对象调用next。不过如果你认为可以对返回对象调用next()属于实现细节Iterable有时反而是更好的选择。另一方面如果希望生成器通过send方法接受值或返回一个值则应使用Generator[YieldType, SendType, ReturnType]泛型类型而不是Iterator或Iterable。例如def echo_round() - Generator[int, float, str]: sent yield 0 while sent 0: sent yield round(sent) return Done注意与 typing 模块中许多其他泛型不同Generator的SendType是**逆变contravariant**的既不是协变也不是不变。如果不打算接收或返回任何值就把SendType或ReturnType设置为None。例如可以把第一个例子注解为def squares(n: int) - Generator[int, None, None]: for i in range(n): yield i * i这与使用Iterator[int]或Iterable[int]略有不同因为生成器拥有close、send和throw方法而普通泛型迭代器/可迭代对象没有。如果你计划在返回的生成器上调用这些方法就应使用Generator类型而不是Iterator或Iterable。结语如何选择正确的类型形式场景推荐类型实例或子类实例类名如A无法确定类型 / 动态交互代码Any慎用会隐藏 bug任意值但只做通用操作object固定长度、类型各异的序列tuple[T1, ..., Tn]变长、不可变序列tuple[T, ...]或更优的Sequence[T]函数、回调、lambdaCallable[[...], Rt]多个候选类型之一int | str旧版本用Union可能为Noneint | None即Optional[int]冗长类型简写类型别名隐式赋值 /type语句 /TypeAlias带字段名的固定结构命名元组NamedTuple类对象而非实例type[C]可配合TypeVar(bound...)生成器Iterator[Y]/Iterable[Y]/Generator[Y, S, R]本文的每一类类型都在仓库源码中有对应实现如 mypy/types.py 的AnyType、mypy/types.py 的UnionType、mypy/types.py 的TypeType等测试用例则覆盖在 test-data/unit 下的check-*.test文件中如check-unions.test、check-tuples.test、check-namedtuple.test、check-literal.test等有兴趣的读者可以继续深入阅读。【免费下载链接】mypyOptional static typing for Python项目地址: https://gitcode.com/GitHub_Trending/my/mypy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表