深入了解Python中类型检查的终极指南

Written by

in

文章目录
  • 所有编程语言都包含某种形式的类型系统,用于形式化地规定该语言可以处理哪些类别的对象,以及如何对待这些类别。例如,一个类型系统可以定义“数值类型”,而数字 42 就是数值类型对象的一个具体实例。
  • Python 是一门动态类型语言。这意味着 Python 解释器仅在代码运行时才进行类型检查,并且变量的类型在其生命周期内是可以改变的。以下两个简单示例展示了 Python 的动态类型特性: >>> if False: … 1 + “two” # 这一行永远不会执行,因此不会抛出 TypeError … else: … 1 + 2 … 3 >>> 1 + “two” # 现在这行会被执行并进行类型检查,于是抛出 TypeError TypeError: unsupported operand type(s) for +: ‘int’ and ‘str’ 在第一个例子中,分支 1 + “two” 永远不会被执行,因此也永远不会被类型检查。第二个例子则表明,当表达式 1 + “two” 被求值时,会抛出 TypeError,因为在 Python 中不能将整数和字符串相加。 接下来,我们看看变量是否可以改变其类型: >>> thing = “Hello” >>> type(thing) <class ‘str’> >>> thing = 28.1 >>> type(thing) <class ‘float’> type() 函数返回一个对象的类型。上述例子清楚地表明,变量 thing 的类型是可以改变的,而且 Python 能够在类型变化时正确推断出当前的类型。
  • 与动态类型相对的是静态类型。静态类型检查是在程序运行之前进行的,通常在编译阶段完成。在大多数静态类型语言(如 C 和 Java)中,类型检查正是在编译过程中完成的。 在静态类型语言中,变量通常不允许在后续更改其类型,尽管某些语言可能提供类型转换(casting)机制,允许将变量显式转换为其他类型。 让我们看一个静态类型语言的简短示例。以下是 Java 中的一段代码: String thing; thing = “Hello”; 第一行声明了变量名 thing 在编译时就被绑定到 String 类型。此后,该名称不能再被重新绑定到其他类型。第二行给 thing 赋了一个值,这个值必须是一个 String 对象。例如,如果你之后写 thing = 28.1f,编译器就会报错,因为浮点数与 String 类型不兼容。 Python 始终会是一门动态类型语言。不过,PEP 484 引入了类型提示(type hints),使得对 Python 代码进行静态类型检查成为可能。 需要特别注意的是:与大多数静态类型语言不同,Python 中的类型提示本身不会让解释器强制执行类型约束。正如其名所示,类型提示只是“提示”——它们并不改变 Python 的运行时行为。真正执行静态类型检查的是其他工具(稍后你会看到),这些工具会利用类型提示来分析代码。
  • 在讨论 Python 时,另一个经常出现的概念是“鸭子类型”。这一说法源自一句谚语:“如果它走起来像鸭子,叫起来也像鸭子,那它就是一只鸭子”(或其各种变体)。 鸭子类型是一种与动态类型相关的理念:对象的具体类型或所属类并不重要,重要的是它提供了哪些方法。使用鸭子类型时,你完全不需要检查类型,而是检查对象是否具有某个特定的方法或属性。 举个例子,你可以在任何定义了 .len() 方法的 Python 对象上调用 len(): >>> class TheHobbit: … def __len__(self): … return 95022 … >>> the_hobbit = TheHobbit() >>> len(the_hobbit) 95022 注意,对 len() 的调用实际上返回的是 .len() 方法的返回值。事实上,len() 的内部实现本质上等价于: def len(obj): return obj.__len__() 因此,要调用 len(obj),对 obj 唯一真正的约束就是它必须定义了 .len() 方法。除此之外,obj 可以是任意类型——无论是 str、list、dict,还是我们自定义的 TheHobbit 类。 在对 Python 代码进行静态类型检查时,鸭子类型可以通过结构化子类型(structural subtyping) 得到一定程度的支持。关于鸭子类型的更多内容,我们将在后文进一步探讨。
  • 在本节中,你将学习如何为函数添加类型提示。下面这个函数的作用是将一段文本字符串转换成标题形式:它会正确地进行首字母大写,并添加一条装饰性的分隔线: def headline(text, align=True): if align: return f”{text.title()}n{‘-‘ * len(text)}” else: return f” {text.title()} “.center(50, “o”) 默认情况下,该函数返回一个左对齐的标题,并在其下方加上一条由连字符组成的下划线。如果将 align 参数设为 False,则标题会被居中显示,并用字母 o 构成上下包围的装饰线: >>> print(headline(“python type checking”)) Python Type Checking ——————– >>> print(headline(“python type checking”, align=False)) oooooooooooooo Python Type Checking oooooooooooooo 现在,是时候添加我们的第一个类型提示了!要为函数补充类型信息,只需对其参数和返回值进行注解即可: def headline(text: str, align: bool = True) -> str: … 这里的 text: str 表示参数 text 应该是 str 类型;同样,可选参数 align 应为 bool 类型,默认值为 True;而 -> str 则说明 headline() 函数将返回一个字符串。关于代码风格,PEP 8 建议如下: 冒号前后遵循常规规则:冒号前无空格,冒号后有一个空格,例如 text: str。 当参数注解与默认值同时出现时,在等号 = 两侧加空格:align: bool = True。 在返回类型箭头 -> 两侧也应有空格:def headline(…) -> str。 需要强调的是,像这样添加类型提示不会对程序运行时产生任何影响——它们仅仅是提示,Python 解释器本身并不会强制执行这些类型约束。例如,如果我们给(命名不太恰当的)align 参数传入一个错误的类型,代码依然能正常运行,没有任何报错或警告: >>> print(headline("python type checking", align="left"))Python Type Checking——————– 注意:这段代码看似“有效”,是因为字符串 “left” 在布尔上下文中被视为真值(truthy)。但如果你传入 align=“center”,虽然 “center” 同样是真值,却无法实现你预期的居中效果,反而会造成逻辑混淆。 要捕获这类错误,就需要使用静态类型检查器——即一种在不实际运行代码的情况下分析代码类型是否正确的工具。 你可能已经在编辑器中内置了这样的检查器。例如,PyCharm 会立即给出警告: Expected type 'bool', got 'str' instead 不过,最常用的类型检查工具是 mypy。稍后你会看到 mypy 的简要介绍,后续章节还会深入讲解其工作原理。如果你系统中尚未安装 mypy,可以通过 pip 安装: pip install mypy 接下来,将以下代码保存到名为 headlines.py 的文件中: # headlines.py def headline(text: str, align: bool = True) -> str: if align: return f”{text.title()}n{‘-‘ * len(text)}” else: return f” {text.title()} “.center(50, “o”) print(headline(“python type checking”)) print(headline(“use mypy”, align=”center”)) 这基本上就是前面展示过的代码:包含 headline() 的定义以及两次调用示例。 现在,用 mypy 检查这段代码: $ mypy headlines.py headlines.py:10: error: Argument “align” to “headline” has incompatible type “str”; expected “bool” 根据类型提示,mypy 明确指出:第 10 行传给 headline 的 align 参数类型不匹配——你传入了 str,但函数期望的是 bool。 要修复这个问题,你需要修改传入 align 的值。此外,也可以将参数名 align 改为更清晰、不易误解的名字,比如 centered: # headlines.py def headline(text: str, centered: bool = False) -> str: if not centered: return f”{text.title()}n{‘-‘ * len(text)}” else: return f” {text.title()} “.center(50, “o”) print(headline(“python type checking”)) print(headline(“use mypy”, centered=True)) 这里我们将 align 改为 centered,并在调用时正确传入布尔值 True。现在再运行 mypy: $ mypy headlines.pySuccess: no issues found in 1 source file 这条成功消息表明:mypy 未检测到任何类型错误。(旧版本的 mypy 在无错误时会直接静默输出,不显示任何内容。) 最后,运行程序本身,你会看到预期的输出: $ python headlines.pyPython Type Checking——————–oooooooooooooooooooo Use Mypy oooooooooooooooooooo 第一个标题左对齐,第二个标题居中显示——一切如预期般工作。
  • 上一节让你初步体验了 Python 中类型检查的实际效果。你也看到了为代码添加类型的一个明显优势:类型提示有助于捕获某些错误。除此之外,还有其他几项重要优点: 类型提示有助于文档化你的代码:传统上,如果你希望说明函数参数的预期类型,通常会使用文档字符串(docstrings)。虽然这可行,但由于 PEP 257 并未为 docstring 中的类型描述建立统一标准,这类信息难以被工具自动解析和用于静态检查。 类型提示能显著提升 IDE 和代码检查工具(linter)的能力:它们让工具更容易对代码进行静态推理。例如,有了类型注解后,PyCharm 就知道 text 是一个字符串,从而提供更精准的代码补全建议. 类型提示有助于构建和维护更清晰的代码架构:编写类型提示的过程会促使你主动思考程序中各部分的数据类型。尽管 Python 的动态特性是其强大之处,但有意识地审视自己是否过度依赖鸭子类型、方法重载或多类型返回值,是一种良好的工程习惯。 当然,静态类型检查并非完美无缺,也存在一些需要权衡的缺点: 添加类型提示需要额外的开发时间和精力:虽然长远来看可能减少调试时间,但在编写代码时确实会增加输入成本。 类型提示在较新的 Python 版本中效果最佳:类型注解最早在 Python 3.0 引入,Python 2.7 可通过类型注释(type comments)实现有限支持。但像变量注解(variable annotations)和类型提示的延迟求值(postponed evaluation)等关键改进,直到 Python 3.6 甚至 3.7 才真正成熟。因此,在旧版本中使用类型提示体验会打折扣。 类型提示会带来轻微的启动性能开销:如果你在代码中导入了 typing 模块,其导入时间可能较为显著——尤其在短小脚本中更为明显。 那么,你是否应该在自己的项目中使用静态类型检查呢?其实,这并不是一个“全有或全无”的选择。幸运的是,Python 支持渐进式类型(gradual typing) 的理念:你可以逐步为代码添加类型提示。静态类型检查器会忽略没有类型注解的部分,因此你可以先从关键模块开始引入类型,只要它对你有价值,就继续推进。 回顾上述优缺点列表,你会发现:添加类型提示对程序的运行行为和最终用户完全没有任何影响。类型检查的唯一目的,就是让你作为开发者的工作更轻松、更高效。 以下是一些实用的经验法则,帮助你判断是否应在项目中使用类型提示: 如果你刚开始学习 Python,完全可以暂缓使用类型提示,等积累更多经验后再考虑。 对于一次性使用的短脚本,类型提示带来的价值非常有限。 对于会被他人使用的库(尤其是发布到 PyPI 的库),类型提示极具价值。其他项目在使用你的库时,依赖这些类型提示才能进行完整的类型检查。已采用类型提示的知名项目包括 cursive_re、black、Real Python 自家的 Reader 应用,以及 mypy 本身。 在大型项目中,类型提示能帮助你理清类型在代码中的流动逻辑,强烈推荐使用;如果项目涉及多人协作,其价值则更加突出。 正如 Bernát Gábor 在其精彩文章《The State of Type Hints in Python》中所建议的:“只要值得编写单元测试的地方,就应该使用类型提示。” 事实上,类型提示在代码中扮演的角色与测试类似——它们都是帮助你写出更健壮、更可维护代码的辅助手段。 希望你现在对 Python 中的类型检查机制有了清晰的理解,并能判断它是否适合你的项目。
  • 注解(Annotations)最早在 Python 3.0 中引入,最初并没有特定用途,仅仅是一种将任意表达式与函数参数和返回值关联起来的机制。 多年后,PEP 484 基于 Jukka Lehtosalo 在其博士项目(即 mypy)中的工作,正式定义了如何在 Python 代码中添加类型提示(type hints)。而实现类型提示的主要方式,正是通过注解。随着类型检查日益普及,注解如今也应主要保留用于类型提示这一目的。 接下来的几节将详细解释在类型提示上下文中,注解是如何工作的。
  • 对于函数,你可以为参数和返回值添加注解。语法如下: def func(arg: arg_type, optarg: arg_type = default) -> return_type: … 参数注解使用 参数名: 注解 的形式; 返回值注解则使用 -> 注解 的形式。 需要注意的是,注解必须是合法的 Python 表达式。 下面是一个简单示例,为计算圆周长的函数添加了类型注解: import math def circumference(radius: float) -> float: return 2 * math.pi * radius 运行代码时,你还可以查看这些注解。它们被存储在函数的一个特殊属性 annotations 中: >>> circumference(1.23) 7.728317927830891 >>> circumference.__annotations__ {‘radius’: <class ‘float’>, ‘return’: <class ‘float’>} 有时你可能会疑惑 mypy 是如何理解你的类型提示的。为此,mypy 提供了两个特殊的调试工具:reveal_type() 和 reveal_locals()。你可以在运行 mypy 前将它们插入代码中,mypy 会如实报告它所推断出的类型。 例如,将以下代码保存为 reveal.py: # reveal.py import math reveal_type(math.pi) radius = 1 circumference = 2 * math.pi * radius reveal_locals() 然后用 mypy 运行它: $ mypy reveal.py reveal.py:4: error: Revealed type is ‘builtins.float’ reveal.py:8: error: Revealed local types are: reveal.py:8: error: circumference: builtins.float reveal.py:8: error: radius: builtins.int 即使没有任何显式注解,mypy 也能正确推断出内置常量 math.pi 以及局部变量 radius 和 circumference 的类型。
  • 在上一节的 circumference() 函数中,我们只注解了参数和返回值,并未在函数体内添加任何变量注解——这通常已经足够。但有时,类型检查器也需要帮助来推断变量的类型。为此,PEP 526 在 Python 3.6 中引入了变量注解,其语法与函数参数注解一致: pi: float = 3.142 def circumference(radius: float) -> float: return 2 * pi * radius 这里,变量 pi 被注解为 float 类型。 注意:静态类型检查器完全可以从字面量 3.142 推断出它是 float,因此这个例子中的注解并非必需。随着你对 Python 类型系统了解加深,会遇到更多真正需要变量注解的场景。 变量注解会被存储在模块级别的 annotations 字典中: >>> circumference(1) 6.284 >>> __annotations__ {‘pi’: <class ‘float’>} 你甚至可以只声明注解而不赋值。此时,注解会被加入 annotations,但变量本身并未定义: >>> nothing: str >>> nothing NameError: name ‘nothing’ is not defined >>> __annotations__ {‘nothing’: <class ‘str’>} 由于没有给 nothing 赋值,该名称在运行时并不存在。
  • 如前所述,注解是在 Python 3 中引入的,并未向后兼容到 Python 2。因此,如果你的代码需要支持旧版 Python(如 2.7),就无法使用注解。 这时可以使用类型注释(type comments) ——一种特殊格式的注释,用于在旧代码中添加类型提示。 例如,为函数添加类型注释的方式如下: import math def circumference(radius): # type: (float) -> float return 2 * math.pi * radius 类型注释本质上只是普通注释,因此可在任何 Python 版本中使用。 但请注意:类型注释由类型检查器直接处理,不会出现在 annotations 字典中: >>> circumference.__annotations__ {} 类型注释必须以 # type: 开头,并且必须位于函数定义的同一行或下一行。如果函数有多个参数,用逗号分隔各类型: def headline(text, width=80, fill_char=”-“): # type: (str, int, str) -> str return f” {text.title()} “.center(width, fill_char) print(headline(“type comments work”, width=40)) 你也可以为每个参数单独写一行注释: # headlines.py def headline( text, # type: str width=80, # type: int fill_char=”-“, # type: str ): # type: (…) -> str return f” {text.title()} “.center(width, fill_char) print(headline(“type comments work”, width=40)) 分别用 Python 和 mypy 运行: $ python headlines.py ———- Type Comments Work ———- $ mypy headlines.py Success: no issues found in 1 source file 如果出现类型错误(例如第 10 行调用 headline() 时传入 width=“full”),mypy 会准确报错: $ mypy headline.pyheadline.py:10: error: Argument "width" to "headline" has incompatible                       type "str"; expected "int" 变量也可以使用类型注释: pi = 3.142 # type: float 这样,pi 就会被类型检查器视为 float 类型。
  • 到目前为止,你只在类型提示中使用了像 str、float 和 bool 这样的基本类型。但实际上,Python 的类型系统非常强大,支持许多更复杂的类型。这是必要的——因为 Python 本质上是动态的、基于鸭子类型的语言,类型系统必须能够合理地建模这种灵活性。 在本节中,你将通过实现一个简单的纸牌游戏,深入学习 Python 的类型系统。你会看到如何指定: 序列(如元组、列表)和映射(如字典)的类型 让代码更易读的类型别名(type aliases) 表示函数或方法不返回任何值的方式 表示对象可以是任意类型的机制 在简要探讨一些类型理论之后,你还会看到更多在 Python 中指定类型的方法。本节的代码示例可在此处找到。
  • 下面的示例实现了一副扑克牌: # game.py import random SUITS = “♠ ♡ ♢ ♣”.split() RANKS = “2 3 4 5 6 7 8 9 10 J Q K A”.split() def create_deck(shuffle=False): “””创建一副新的 52 张牌””” deck = [(s, r) for r in RANKS for s in SUITS] if shuffle: random.shuffle(deck) return deck def deal_hands(deck): “””将牌平均发给四位玩家””” return (deck[0::4], deck[1::4], deck[2::4], deck[3::4]) def play(): “””运行一个四人纸牌游戏””” deck = create_deck(shuffle=True) names = “P1 P2 P3 P4″.split() hands = {n: h for n, h in zip(names, deal_hands(deck))} for name, cards in hands.items(): card_str = ” “.join(f”{s}{r}” for (s, r) in cards) print(f”{name}: {card_str}”) if __name__ == “__main__”: play() 每张牌用一个包含两个字符串的元组表示:(花色, 点数)。 整副牌是一个牌的列表。 create_deck() 创建一副 52 张的标准牌,并可选择是否洗牌。 deal_hands() 将牌平均分给四位玩家。 play() 目前只是准备游戏:洗牌并分发,尚未实现具体玩法。 典型输出如下: $ python game.pyP4: ♣9 ♢9 ♡2 ♢7 ♡7 ♣A ♠6 ♡K ♡5 ♢6 ♢3 ♣3 ♣QP1: ♡A ♠2 ♠10 ♢J ♣10 ♣4 ♠5 ♡Q ♢5 ♣6 ♠A ♣5 ♢4P2: ♢2 ♠7 ♡8 ♢K ♠3 ♡3 ♣K ♠J ♢A ♣7 ♡6 ♡10 ♠KP3: ♣2 ♣8 ♠8 ♣J ♢Q ♡9 ♡J ♠4 ♢8 ♢10 ♠9 ♡4 ♠Q
  • 当处理嵌套类型(如 List[Tuple[str, str]])时,类型提示会变得晦涩难懂。想象一下 deal_hands() 的原始注解: def deal_hands( deck: List[Tuple[str, str]] ) -> Tuple[ List[Tuple[str, str]], List[Tuple[str, str]], List[Tuple[str, str]], List[Tuple[str, str]], ]: … 简直难以阅读! 由于类型注解是合法的 Python 表达式,你可以定义类型别名: from typing import List, Tuple Card = Tuple[str, str] Deck = List[Card] 现在,deal_hands() 的注解变得清晰: def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]: “””将牌平均发给四位玩家””” return (deck[0::4], deck[1::4], deck[2::4], deck[3::4]) 类型别名不仅提升可读性,还能被检查其真实含义: >>> Deck typing.List[typing.Tuple[str, str]]
  • 让我们回到实践。你还记得之前尝试为通用的 choose() 函数添加类型注解时遇到的问题: import random from typing import Any, Sequence def choose(items: Sequence[Any]) -> Any: return random.choice(items) 使用 Any 的问题是:你无谓地丢失了类型信息。你知道,如果传入一个字符串列表,choose() 应该返回一个字符串。下面我们将学习如何用类型变量(Type Variables)来精确表达这种关系,并进一步探讨: 鸭子类型与协议(Protocols) 默认值为 None 的参数 类方法的类型注解 自定义类作为类型 可变数量参数(*args, **kwargs) 可调用对象(Callables)
  • typing 的引入是为了让 Python 在保持动态语言灵活性的同时,获得静态类型语言的安全性和工程化优势 常见使用场景 为函数参数和返回值指定预期类型 用精确的类型信息定义复杂的数据结构 提高代码的可读性与可维护性 协助 mypy 等静态类型检查工具识别潜在错误 以上就是深入了解Python中类型检查的终极指南的详细内容,更多关于Python类型检查的资料请关注风君子博客其它相关文章! 您可能感兴趣的文章: Python中的函数参数类型检查 Python使用描述符实现属性类型检查的案例解析 Python静态类型检查新工具之pyright 使用指南 python如何对实例属性进行类型检查 Python中类型检查的详细介绍 在Python 3中实现类型检查器的简单方法 Python中实现参数类型检查的简单方法
  • 目录
    • 类型系统(Type Systems)
    • 动态类型(Dynamic Typing)
    • 静态类型(Static Typing)
    • 鸭子类型(Duck Typing)
    • 初识类型(Hello Types)
    • 优缺点分析(Pros and Cons)
    • 注解(Annotations)
    • 函数注解(Function Annotations)
    • 变量注解(Variable Annotations)
    • 类型注释(Type Comments)
      • 那么,该用注解还是类型注释
    • 玩转 Python 类型(第一部分)
      • 示例:一副纸牌
        • 序列与映射(Sequences and Mappings)
      • 类型别名(Type Aliases)
        • 无返回值的函数(Functions Without Return Values)
        • 示例:开始出牌
        • Any 类型
      • 玩转 Python 类型(第二部分)
        • 类型变量(Type Variables)
        • 约束类型变量
      • 总结

        在本指南中,你将深入了解 Python 的类型检查机制。传统上,Python 解释器以一种灵活但隐式的方式来处理类型。而近年来的 Python 版本允许你显式地添加类型提示(type hints),这些提示可以被各种工具利用,从而帮助你更高效地开发代码。

        在本教程中,你将学习以下内容:

        • 类型注解(Type annotations)与类型提示(type hints)
        • 如何为代码(包括你自己的代码和他人的代码)添加静态类型
        • 如何运行静态类型检查器
        • 如何在运行时强制执行类型约束

        这是一份内容详尽的指南,涵盖范围较广。如果你只是想快速了解 Python 中类型提示的基本用法,并判断类型检查是否值得引入到你的项目中,那么你并不需要通读全文。其中“初识类型(Hello Types)”和“优缺点分析(Pros and Cons)”这两个章节,就能让你初步体会到类型检查的工作方式,并为你提供何时使用它更为合适的建议。

        所有编程语言都包含某种形式的类型系统,用于形式化地规定该语言可以处理哪些类别的对象,以及如何对待这些类别。例如,一个类型系统可以定义“数值类型”,而数字 42 就是数值类型对象的一个具体实例。

        Python 是一门动态类型语言。这意味着 Python 解释器仅在代码运行时才进行类型检查,并且变量的类型在其生命周期内是可以改变的。以下两个简单示例展示了 Python 的动态类型特性:

        >>> if False:
        ...     1 + "two"  # 这一行永远不会执行,因此不会抛出 TypeError
        ... else:
        ...     1 + 2
        ...
        3
        
        >>> 1 + "two"  # 现在这行会被执行并进行类型检查,于是抛出 TypeError
        TypeError: unsupported operand type(s) for +: 'int' and 'str'
        

        在第一个例子中,分支 1 + “two” 永远不会被执行,因此也永远不会被类型检查。第二个例子则表明,当表达式 1 + “two” 被求值时,会抛出 TypeError,因为在 Python 中不能将整数和字符串相加。

        接下来,我们看看变量是否可以改变其类型:

        >>> thing = "Hello"
        >>> type(thing)
        <class 'str'>
        
        >>> thing = 28.1
        >>> type(thing)
        <class 'float'>
        

        type() 函数返回一个对象的类型。上述例子清楚地表明,变量 thing 的类型是可以改变的,而且 Python 能够在类型变化时正确推断出当前的类型。

        与动态类型相对的是静态类型。静态类型检查是在程序运行之前进行的,通常在编译阶段完成。在大多数静态类型语言(如 C 和 Java)中,类型检查正是在编译过程中完成的。

        在静态类型语言中,变量通常不允许在后续更改其类型,尽管某些语言可能提供类型转换(casting)机制,允许将变量显式转换为其他类型。

        让我们看一个静态类型语言的简短示例。以下是 Java 中的一段代码:

        String thing;
        thing = "Hello";
        

        第一行声明了变量名 thing 在编译时就被绑定到 String 类型。此后,该名称不能再被重新绑定到其他类型。第二行给 thing 赋了一个值,这个值必须是一个 String 对象。例如,如果你之后写 thing = 28.1f,编译器就会报错,因为浮点数与 String 类型不兼容。

        Python 始终会是一门动态类型语言。不过,PEP 484 引入了类型提示(type hints),使得对 Python 代码进行静态类型检查成为可能。

        需要特别注意的是:与大多数静态类型语言不同,Python 中的类型提示本身不会让解释器强制执行类型约束。正如其名所示,类型提示只是“提示”——它们并不改变 Python 的运行时行为。真正执行静态类型检查的是其他工具(稍后你会看到),这些工具会利用类型提示来分析代码。

        在讨论 Python 时,另一个经常出现的概念是“鸭子类型”。这一说法源自一句谚语:“如果它走起来像鸭子,叫起来也像鸭子,那它就是一只鸭子”(或其各种变体)。

        鸭子类型是一种与动态类型相关的理念:对象的具体类型或所属类并不重要,重要的是它提供了哪些方法。使用鸭子类型时,你完全不需要检查类型,而是检查对象是否具有某个特定的方法或属性。

        举个例子,你可以在任何定义了 .len() 方法的 Python 对象上调用 len():

        >>> class TheHobbit:
        ...     def __len__(self):
        ...         return 95022
        ...
        >>> the_hobbit = TheHobbit()
        >>> len(the_hobbit)
        95022
        

        注意,对 len() 的调用实际上返回的是 .len() 方法的返回值。事实上,len() 的内部实现本质上等价于:

        def len(obj):
            return obj.__len__()
        

        因此,要调用 len(obj),对 obj 唯一真正的约束就是它必须定义了 .len() 方法。除此之外,obj 可以是任意类型——无论是 str、list、dict,还是我们自定义的 TheHobbit 类。

        在对 Python 代码进行静态类型检查时,鸭子类型可以通过结构化子类型(structural subtyping) 得到一定程度的支持。关于鸭子类型的更多内容,我们将在后文进一步探讨。

        在本节中,你将学习如何为函数添加类型提示。下面这个函数的作用是将一段文本字符串转换成标题形式:它会正确地进行首字母大写,并添加一条装饰性的分隔线:

        def headline(text, align=True):
            if align:
                return f"{text.title()}n{'-' * len(text)}"
            else:
                return f" {text.title()} ".center(50, "o")
        

        默认情况下,该函数返回一个左对齐的标题,并在其下方加上一条由连字符组成的下划线。如果将 align 参数设为 False,则标题会被居中显示,并用字母 o 构成上下包围的装饰线:

        >>> print(headline("python type checking"))
        Python Type Checking
        --------------------
        
        >>> print(headline("python type checking", align=False))
        oooooooooooooo Python Type Checking oooooooooooooo
        

        现在,是时候添加我们的第一个类型提示了!要为函数补充类型信息,只需对其参数和返回值进行注解即可:

        def headline(text: str, align: bool = True) -> str:
            ...
        

        这里的 text: str 表示参数 text 应该是 str 类型;同样,可选参数 align 应为 bool 类型,默认值为 True;而 -> str 则说明 headline() 函数将返回一个字符串。
        关于代码风格,PEP 8 建议如下:

        • 冒号前后遵循常规规则:冒号前无空格,冒号后有一个空格,例如 text: str。
        • 当参数注解与默认值同时出现时,在等号 = 两侧加空格:align: bool = True。
        • 在返回类型箭头 -> 两侧也应有空格:def headline(…) -> str。

        需要强调的是,像这样添加类型提示不会对程序运行时产生任何影响——它们仅仅是提示,Python 解释器本身并不会强制执行这些类型约束。例如,如果我们给(命名不太恰当的)align 参数传入一个错误的类型,代码依然能正常运行,没有任何报错或警告:

        >>> print(headline("python type checking", align="left"))
        Python Type Checking
        ——————–

        注意:这段代码看似“有效”,是因为字符串 “left” 在布尔上下文中被视为真值(truthy)。但如果你传入 align=“center”,虽然 “center” 同样是真值,却无法实现你预期的居中效果,反而会造成逻辑混淆。

        要捕获这类错误,就需要使用静态类型检查器——即一种在不实际运行代码的情况下分析代码类型是否正确的工具。

        你可能已经在编辑器中内置了这样的检查器。例如,PyCharm 会立即给出警告:

        Expected type 'bool', got 'str' instead

        不过,最常用的类型检查工具是 mypy。稍后你会看到 mypy 的简要介绍,后续章节还会深入讲解其工作原理。
        如果你系统中尚未安装 mypy,可以通过 pip 安装:

        pip install mypy
        

        接下来,将以下代码保存到名为 headlines.py 的文件中:

        # headlines.py
        
        def headline(text: str, align: bool = True) -> str:
            if align:
                return f"{text.title()}n{'-' * len(text)}"
            else:
                return f" {text.title()} ".center(50, "o")
        
        print(headline("python type checking"))
        print(headline("use mypy", align="center"))
        

        这基本上就是前面展示过的代码:包含 headline() 的定义以及两次调用示例。

        现在,用 mypy 检查这段代码:

        $ mypy headlines.py
        headlines.py:10: error: Argument "align" to "headline" has incompatible
                                type "str"; expected "bool"
        

        根据类型提示,mypy 明确指出:第 10 行传给 headline 的 align 参数类型不匹配——你传入了 str,但函数期望的是 bool。

        要修复这个问题,你需要修改传入 align 的值。此外,也可以将参数名 align 改为更清晰、不易误解的名字,比如 centered:

        # headlines.py
        
        def headline(text: str, centered: bool = False) -> str:
            if not centered:
                return f"{text.title()}n{'-' * len(text)}"
            else:
                return f" {text.title()} ".center(50, "o")
        
        print(headline("python type checking"))
        print(headline("use mypy", centered=True))
        

        这里我们将 align 改为 centered,并在调用时正确传入布尔值 True。现在再运行 mypy:

        $ mypy headlines.py
        Success: no issues found in 1 source file

        这条成功消息表明:mypy 未检测到任何类型错误。(旧版本的 mypy 在无错误时会直接静默输出,不显示任何内容。)

        最后,运行程序本身,你会看到预期的输出:

        $ python headlines.py
        Python Type Checking
        ——————–
        oooooooooooooooooooo Use Mypy oooooooooooooooooooo

        第一个标题左对齐,第二个标题居中显示——一切如预期般工作。

        上一节让你初步体验了 Python 中类型检查的实际效果。你也看到了为代码添加类型的一个明显优势:类型提示有助于捕获某些错误。除此之外,还有其他几项重要优点:

        • 类型提示有助于文档化你的代码:传统上,如果你希望说明函数参数的预期类型,通常会使用文档字符串(docstrings)。虽然这可行,但由于 PEP 257 并未为 docstring 中的类型描述建立统一标准,这类信息难以被工具自动解析和用于静态检查。
        • 类型提示能显著提升 IDE 和代码检查工具(linter)的能力:它们让工具更容易对代码进行静态推理。例如,有了类型注解后,PyCharm 就知道 text 是一个字符串,从而提供更精准的代码补全建议.
        • 类型提示有助于构建和维护更清晰的代码架构:编写类型提示的过程会促使你主动思考程序中各部分的数据类型。尽管 Python 的动态特性是其强大之处,但有意识地审视自己是否过度依赖鸭子类型、方法重载或多类型返回值,是一种良好的工程习惯。

        当然,静态类型检查并非完美无缺,也存在一些需要权衡的缺点:

        • 添加类型提示需要额外的开发时间和精力:虽然长远来看可能减少调试时间,但在编写代码时确实会增加输入成本。
        • 类型提示在较新的 Python 版本中效果最佳:类型注解最早在 Python 3.0 引入,Python 2.7 可通过类型注释(type comments)实现有限支持。但像变量注解(variable annotations)和类型提示的延迟求值(postponed evaluation)等关键改进,直到 Python 3.6 甚至 3.7 才真正成熟。因此,在旧版本中使用类型提示体验会打折扣。
        • 类型提示会带来轻微的启动性能开销:如果你在代码中导入了 typing 模块,其导入时间可能较为显著——尤其在短小脚本中更为明显。

        那么,你是否应该在自己的项目中使用静态类型检查呢?其实,这并不是一个“全有或全无”的选择。幸运的是,Python 支持渐进式类型(gradual typing) 的理念:你可以逐步为代码添加类型提示。静态类型检查器会忽略没有类型注解的部分,因此你可以先从关键模块开始引入类型,只要它对你有价值,就继续推进。

        回顾上述优缺点列表,你会发现:添加类型提示对程序的运行行为和最终用户完全没有任何影响。类型检查的唯一目的,就是让你作为开发者的工作更轻松、更高效。

        以下是一些实用的经验法则,帮助你判断是否应在项目中使用类型提示:

        • 如果你刚开始学习 Python,完全可以暂缓使用类型提示,等积累更多经验后再考虑。
        • 对于一次性使用的短脚本,类型提示带来的价值非常有限。
        • 对于会被他人使用的库(尤其是发布到 PyPI 的库),类型提示极具价值。其他项目在使用你的库时,依赖这些类型提示才能进行完整的类型检查。已采用类型提示的知名项目包括 cursive_re、black、Real Python 自家的 Reader 应用,以及 mypy 本身。
        • 在大型项目中,类型提示能帮助你理清类型在代码中的流动逻辑,强烈推荐使用;如果项目涉及多人协作,其价值则更加突出。

        正如 Bernát Gábor 在其精彩文章《The State of Type Hints in Python》中所建议的:“只要值得编写单元测试的地方,就应该使用类型提示。” 事实上,类型提示在代码中扮演的角色与测试类似——它们都是帮助你写出更健壮、更可维护代码的辅助手段。

        希望你现在对 Python 中的类型检查机制有了清晰的理解,并能判断它是否适合你的项目。

        注解(Annotations)最早在 Python 3.0 中引入,最初并没有特定用途,仅仅是一种将任意表达式与函数参数和返回值关联起来的机制。

        多年后,PEP 484 基于 Jukka Lehtosalo 在其博士项目(即 mypy)中的工作,正式定义了如何在 Python 代码中添加类型提示(type hints)。而实现类型提示的主要方式,正是通过注解。随着类型检查日益普及,注解如今也应主要保留用于类型提示这一目的。

        接下来的几节将详细解释在类型提示上下文中,注解是如何工作的。

        对于函数,你可以为参数和返回值添加注解。语法如下:

        def func(arg: arg_type, optarg: arg_type = default) -> return_type:
            ...
        
        • 参数注解使用 参数名: 注解 的形式;
        • 返回值注解则使用 -> 注解 的形式。

        需要注意的是,注解必须是合法的 Python 表达式。

        下面是一个简单示例,为计算圆周长的函数添加了类型注解:

        import math
        
        def circumference(radius: float) -> float:
            return 2 * math.pi * radius
        

        运行代码时,你还可以查看这些注解。它们被存储在函数的一个特殊属性 annotations 中:

        >>> circumference(1.23)
        7.728317927830891
        
        >>> circumference.__annotations__
        {'radius': <class 'float'>, 'return': <class 'float'>}
        

        有时你可能会疑惑 mypy 是如何理解你的类型提示的。为此,mypy 提供了两个特殊的调试工具:reveal_type() 和 reveal_locals()。你可以在运行 mypy 前将它们插入代码中,mypy 会如实报告它所推断出的类型。

        例如,将以下代码保存为 reveal.py:

        # reveal.py
        
        import math
        reveal_type(math.pi)
        
        radius = 1
        circumference = 2 * math.pi * radius
        reveal_locals()
        

        然后用 mypy 运行它:

        $ mypy reveal.py
        reveal.py:4: error: Revealed type is 'builtins.float'
        
        reveal.py:8: error: Revealed local types are:
        reveal.py:8: error: circumference: builtins.float
        reveal.py:8: error: radius: builtins.int
        

        即使没有任何显式注解,mypy 也能正确推断出内置常量 math.pi 以及局部变量 radius 和 circumference 的类型。

        在上一节的 circumference() 函数中,我们只注解了参数和返回值,并未在函数体内添加任何变量注解——这通常已经足够。
        但有时,类型检查器也需要帮助来推断变量的类型。为此,PEP 526 在 Python 3.6 中引入了变量注解,其语法与函数参数注解一致:

        pi: float = 3.142
        
        def circumference(radius: float) -> float:
            return 2 * pi * radius
        

        这里,变量 pi 被注解为 float 类型。

        注意:静态类型检查器完全可以从字面量 3.142 推断出它是 float,因此这个例子中的注解并非必需。随着你对 Python 类型系统了解加深,会遇到更多真正需要变量注解的场景。

        变量注解会被存储在模块级别的 annotations 字典中:

        >>> circumference(1)
        6.284
        
        >>> __annotations__
        {'pi': <class 'float'>}
        

        你甚至可以只声明注解而不赋值。此时,注解会被加入 annotations,但变量本身并未定义:

        >>> nothing: str
        >>> nothing
        NameError: name 'nothing' is not defined
        
        >>> __annotations__
        {'nothing': <class 'str'>}
        

        由于没有给 nothing 赋值,该名称在运行时并不存在。

        如前所述,注解是在 Python 3 中引入的,并未向后兼容到 Python 2。因此,如果你的代码需要支持旧版 Python(如 2.7),就无法使用注解。

        这时可以使用类型注释(type comments) ——一种特殊格式的注释,用于在旧代码中添加类型提示。

        例如,为函数添加类型注释的方式如下:

        import math
        
        def circumference(radius):
            # type: (float) -> float
            return 2 * math.pi * radius
        

        类型注释本质上只是普通注释,因此可在任何 Python 版本中使用。

        但请注意:类型注释由类型检查器直接处理,不会出现在 annotations 字典中:

        >>> circumference.__annotations__
        {}
        

        类型注释必须以 # type: 开头,并且必须位于函数定义的同一行或下一行。如果函数有多个参数,用逗号分隔各类型:

        def headline(text, width=80, fill_char="-"):
            # type: (str, int, str) -> str
            return f" {text.title()} ".center(width, fill_char)
        
        print(headline("type comments work", width=40))
        

        你也可以为每个参数单独写一行注释:

        # headlines.py
        
        def headline(
            text,           # type: str
            width=80,       # type: int
            fill_char="-",  # type: str
        ):                  # type: (...) -> str
            return f" {text.title()} ".center(width, fill_char)
        
        print(headline("type comments work", width=40))
        

        分别用 Python 和 mypy 运行:

        $ python headlines.py
        ---------- Type Comments Work ----------
        
        $ mypy headlines.py
        Success: no issues found in 1 source file
        

        如果出现类型错误(例如第 10 行调用 headline() 时传入 width=“full”),mypy 会准确报错:

        $ mypy headline.py
        headline.py:10: error: Argument "width" to "headline" has incompatible
                               type "str"; expected "int"

        变量也可以使用类型注释:

        pi = 3.142  # type: float
        

        这样,pi 就会被类型检查器视为 float 类型。

        当你为自己的代码添加类型提示时,应该选择注解还是类型注释?

        简短回答:能用注解就用注解,只有在必须兼容旧版本时才用类型注释。

        • 注解语法更简洁,将类型信息紧贴代码,可读性更强。它们是官方推荐的类型提示方式,未来也会持续得到支持和改进。
        • 类型注释则更冗长,还可能与代码中的其他注释(如 linter 指令)产生冲突。但它们适用于不支持注解的旧代码库。

        到目前为止,你只在类型提示中使用了像 str、float 和 bool 这样的基本类型。但实际上,Python 的类型系统非常强大,支持许多更复杂的类型。这是必要的——因为 Python 本质上是动态的、基于鸭子类型的语言,类型系统必须能够合理地建模这种灵活性。

        在本节中,你将通过实现一个简单的纸牌游戏,深入学习 Python 的类型系统。你会看到如何指定:

        • 序列(如元组、列表)和映射(如字典)的类型
        • 让代码更易读的类型别名(type aliases)
        • 表示函数或方法不返回任何值的方式
        • 表示对象可以是任意类型的机制

        在简要探讨一些类型理论之后,你还会看到更多在 Python 中指定类型的方法。本节的代码示例可在此处找到。

        下面的示例实现了一副扑克牌:

        # game.py
        
        import random
        
        SUITS = "♠ ♡ ♢ ♣".split()
        RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
        
        def create_deck(shuffle=False):
            """创建一副新的 52 张牌"""
            deck = [(s, r) for r in RANKS for s in SUITS]
            if shuffle:
                random.shuffle(deck)
            return deck
        
        def deal_hands(deck):
            """将牌平均发给四位玩家"""
            return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
        
        def play():
            """运行一个四人纸牌游戏"""
            deck = create_deck(shuffle=True)
            names = "P1 P2 P3 P4".split()
            hands = {n: h for n, h in zip(names, deal_hands(deck))}
        
            for name, cards in hands.items():
                card_str = " ".join(f"{s}{r}" for (s, r) in cards)
                print(f"{name}: {card_str}")
        
        if __name__ == "__main__":
            play()
        
        • 每张牌用一个包含两个字符串的元组表示:(花色, 点数)。
        • 整副牌是一个牌的列表。
        • create_deck() 创建一副 52 张的标准牌,并可选择是否洗牌。
        • deal_hands() 将牌平均分给四位玩家。
        • play() 目前只是准备游戏:洗牌并分发,尚未实现具体玩法。

        典型输出如下:

        $ python game.py
        P4: ♣9 ♢9 ♡2 ♢7 ♡7 ♣A ♠6 ♡K ♡5 ♢6 ♢3 ♣3 ♣Q
        P1: ♡A ♠2 ♠10 ♢J ♣10 ♣4 ♠5 ♡Q ♢5 ♣6 ♠A ♣5 ♢4
        P2: ♢2 ♠7 ♡8 ♢K ♠3 ♡3 ♣K ♠J ♢A ♣7 ♡6 ♡10 ♠K
        P3: ♣2 ♣8 ♠8 ♣J ♢Q ♡9 ♡J ♠4 ♢8 ♢10 ♠9 ♡4 ♠Q

        现在,我们为纸牌游戏添加类型提示,即为 create_deck()、deal_hands() 和 play() 添加注解。

        第一个挑战是:如何注解复合类型?比如表示整副牌的列表,以及表示单张牌的元组。

        对于基本类型,注解很简单:

        >>> name: str = "Guido"
        >>> pi: float = 3.142
        >>> centered: bool = False
        

        对复合类型,你也可以直接使用类型本身:

        >>> names: list = ["Guido", "Jukka", "Ivan"]
        >>> version: tuple = (3, 7, 1)
        >>> options: dict = {"centered": False, "capitalize": True}
        

        但这样写信息不足:我们无法知道 names[2] 是 str、version[0] 是 int、options[“centered”] 是 bool。这些信息对类型检查器来说是缺失的。

        因此,应使用 typing 模块中定义的泛型类型:

        >>> from typing import Dict, List, Tuple
        
        >>> names: List[str] = ["Guido", "Jukka", "Ivan"]
        >>> version: Tuple[int, int, int] = (3, 7, 1)
        >>> options: Dict[str, bool] = {"centered": False, "capitalize": True}
        

        注意:

        • 这些类型首字母大写;
        • 使用方括号指定元素类型。

        具体含义:

        • names 是一个字符串列表;
        • version 是一个包含三个整数的三元组;
        • options 是一个从字符串映射到布尔值的字典。

        typing 模块还提供了更多复合类型,如 Counter、Deque、FrozenSet、NamedTuple、Set 等。

        回到纸牌游戏:一张牌是 (str, str) 元组,整副牌是 List[Tuple[str, str]]。因此,create_deck() 可注解为:

        from typing import List, Tuple
        
        def create_deck(shuffle: bool = False) -> List[Tuple[str, str]]:
            """创建一副新的 52 张牌"""
            deck = [(s, r) for r in RANKS for s in SUITS]
            if shuffle:
                random.shuffle(deck)
            return deck
        

        很多时候,函数只关心参数是“某种序列”,而不关心是列表还是元组。此时应使用 typing.Sequence:

        from typing import List, Sequence
        
        def square(elems: Sequence[float]) -> List[float]:
            return [x**2 for x in elems]
        

        这体现了鸭子类型思想:只要对象支持 len() 和 .getitem(),就是 Sequence。

        当处理嵌套类型(如 List[Tuple[str, str]])时,类型提示会变得晦涩难懂。想象一下 deal_hands() 的原始注解:

        def deal_hands(
            deck: List[Tuple[str, str]]
        ) -> Tuple[
            List[Tuple[str, str]],
            List[Tuple[str, str]],
            List[Tuple[str, str]],
            List[Tuple[str, str]],
        ]:
            ...
        

        简直难以阅读!

        由于类型注解是合法的 Python 表达式,你可以定义类型别名:

        from typing import List, Tuple
        
        Card = Tuple[str, str]
        Deck = List[Card]
        

        现在,deal_hands() 的注解变得清晰:

        def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
            """将牌平均发给四位玩家"""
            return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
        

        类型别名不仅提升可读性,还能被检查其真实含义:

        >>> Deck
        typing.List[typing.Tuple[str, str]]
        

        没有显式 return 的函数实际上返回 None:

        >>> def play(player_name):
        ...     print(f"{player_name} plays")
        ...
        >>> ret_val = play("Jacob")
        Jacob plays
        >>> print(ret_val)
        None
        

        虽然技术上返回了 None,但这个值通常无意义。因此,应明确标注返回类型为 None:

        # play.py
        
        def play(player_name: str) -> None:
            print(f"{player_name} plays")
        
        ret_val = play("Filip")
        

        这样,mypy 能捕获误用返回值的错误:

        $ mypy play.py
        play.py:6: error: "play" does not return a value
        

        如果不加 -> None,mypy 无法推断返回类型,也就不会报错。

        进阶情况:对于永远不会正常返回的函数(如总是抛异常),应使用 NoReturn:

        from typing import NoReturn
        
        def black_hole() -> NoReturn:
            raise Exception("There is no going back ...")
        

        下面是改进版的纸牌游戏:分发手牌后,随机选择起始玩家,然后轮流随机出牌(暂无规则):

        # game.py
        
        import random
        from typing import List, Tuple
        
        SUITS = "♠ ♡ ♢ ♣".split()
        RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
        
        Card = Tuple[str, str]
        Deck = List[Card]
        
        def create_deck(shuffle: bool = False) -> Deck:
            deck = [(s, r) for r in RANKS for s in SUITS]
            if shuffle:
                random.shuffle(deck)
            return deck
        
        def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
            return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
        
        def choose(items):
            """随机选择并返回一个元素"""
            return random.choice(items)
        
        def player_order(names, start=None):
            """调整玩家顺序,使 start 玩家先出"""
            if start is None:
                start = choose(names)
            start_idx = names.index(start)
            return names[start_idx:] + names[:start_idx]
        
        def play() -> None:
            deck = create_deck(shuffle=True)
            names = "P1 P2 P3 P4".split()
            hands = {n: h for n, h in zip(names, deal_hands(deck))}
            start_player = choose(names)
            turn_order = player_order(names, start=start_player)
        
            while hands[start_player]:  # 只要起始玩家还有牌
                for name in turn_order:
                    card = choose(hands[name])
                    hands[name].remove(card)
                    print(f"{name}: {card[0] + card[1]:<3}  ", end="")
                print()
        
        if __name__ == "__main__":
            play()
        

        现在,我们需要为新函数 choose() 和 player_order() 添加类型提示。

        choose() 既可用于玩家名列表,也可用于手牌列表(或其他任何序列)。一种写法是:

        from typing import Any, Sequence
        
        def choose(items: Sequence[Any]) -> Any:
            return random.choice(items)
        

        但这会丢失类型信息。例如:

        # choose.py
        
        names = ["Guido", "Jukka", "Ivan"]
        name = choose(names)
        

        mypy 会推断 names 是 List[str],但 name 的类型却变成 Any:

        $ mypy choose.py
        choose.py:10: error: Revealed type is 'builtins.list[builtins.str*]'
        choose.py:13: error: Revealed type is 'Any'
        

        这意味着后续无法对 name 做字符串操作的类型检查。

        让我们回到实践。你还记得之前尝试为通用的 choose() 函数添加类型注解时遇到的问题:

        import random
        from typing import Any, Sequence
        
        def choose(items: Sequence[Any]) -> Any:
            return random.choice(items)
        

        使用 Any 的问题是:你无谓地丢失了类型信息。你知道,如果传入一个字符串列表,choose() 应该返回一个字符串。下面我们将学习如何用类型变量(Type Variables)来精确表达这种关系,并进一步探讨:

        • 鸭子类型与协议(Protocols)
        • 默认值为 None 的参数
        • 类方法的类型注解
        • 自定义类作为类型
        • 可变数量参数(*args, **kwargs)
        • 可调用对象(Callables)

        类型变量是一种特殊的变量,它可以根据上下文“取”任意类型。

        我们来为 choose() 创建一个类型变量,以准确捕获其行为:

        # choose.py
        
        import random
        from typing import Sequence, TypeVar
        
        Choosable = TypeVar("Choosable")
        
        def choose(items: Sequence[Choosable]) -> Choosable:
            return random.choice(items)
        
        names = ["Guido", "Jukka", "Ivan"]
        reveal_type(names)
        name = choose(names)
        reveal_type(name)
        
        • 类型变量必须通过 typing.TypeVar 定义。
        • 它会根据实际传入的参数“推断”最具体的类型。

        运行 mypy:

        $ mypy choose.py
        choose.py:12: error: Revealed type is 'builtins.list[builtins.str*]'
        choose.py:15: error: Revealed type is 'builtins.str*'
        

        name 现在被正确推断为 str!

        再看几个例子:

        # choose_examples.py
        from choose import choose
        
        reveal_type(choose(["Guido", "Jukka"]))        # str
        reveal_type(choose([1, 2, 3]))                 # int
        reveal_type(choose([True, 42, 3.14]))          # float
        reveal_type(choose(["Python", 3, 7]))          # object
        

        mypy 输出:

        builtins.str*
        builtins.int*
        builtins.float*   # 因为 bool ⊆ int ⊆ float
        builtins.object*  # str 和 int 无共同子类型
        

        注意:最后两个例子没有报错,但类型信息变得模糊。

        如果你希望 choose() 只接受同一种类型的序列(比如只接受全字符串或全数字),可以约束类型变量:

        Choosable = TypeVar("Choosable", str, float)
        
        def choose(items: Sequence[Choosable]) -> Choosable:
            ...
        

        现在:

        • choose([1, 2, 3]) → float(因为 int 是 float 的子类型)
        • choose([“Python”, 3, 7]) → ❌ 类型错误!

        mypy 会报错:

        Value of type variable "Choosable" cannot be "object"

        在纸牌游戏中,我们可以这样约束:

        Choosable = TypeVar("Choosable", str, Card)
        

        这样,choose() 要么处理玩家名(str),要么处理手牌(Card),但不会混合。

        typing 的引入是为了让 Python 在保持动态语言灵活性的同时,获得静态类型语言的安全性和工程化优势

        常见使用场景

        • 为函数参数和返回值指定预期类型
        • 用精确的类型信息定义复杂的数据结构
        • 提高代码的可读性与可维护性
        • 协助 mypy 等静态类型检查工具识别潜在错误

        以上就是深入了解Python中类型检查的终极指南的详细内容,更多关于Python类型检查的资料请关注风君子博客其它相关文章!

        您可能感兴趣的文章:

        • Python中的函数参数类型检查
        • Python使用描述符实现属性类型检查的案例解析
        • Python静态类型检查新工具之pyright 使用指南
        • python如何对实例属性进行类型检查
        • Python中类型检查的详细介绍
        • 在Python 3中实现类型检查器的简单方法
        • Python中实现参数类型检查的简单方法

        站内搜索