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

 更新时间:2026年02月28日 08:59:09   作者:SunnyRivers  
在本指南中,你将深入了解 Python 的类型检查机制,这是一份内容详尽的指南,涵盖范围较广,包括如何运行静态类型检查器,如何在运行时强制执行类型约束等,希望对大家有所帮助

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

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

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

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

类型系统(Type Systems)

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

动态类型(Dynamic Typing)

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 能够在类型变化时正确推断出当前的类型。

静态类型(Static Typing)

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

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

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

String thing;
thing = "Hello";

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

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

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

鸭子类型(Duck Typing)

在讨论 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) 得到一定程度的支持。关于鸭子类型的更多内容,我们将在后文进一步探讨。

初识类型(Hello Types)

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

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

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

优缺点分析(Pros and Cons)

上一节让你初步体验了 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)

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

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

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

函数注解(Function Annotations)

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

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 的类型。

变量注解(Variable Annotations)

在上一节的 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 赋值,该名称在运行时并不存在。

类型注释(Type Comments)

如前所述,注解是在 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 指令)产生冲突。但它们适用于不支持注解的旧代码库。

玩转 Python 类型(第一部分)

到目前为止,你只在类型提示中使用了像 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

序列与映射(Sequences and Mappings)

现在,我们为纸牌游戏添加类型提示,即为 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。

类型别名(Type Aliases)

当处理嵌套类型(如 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]]

无返回值的函数(Functions Without Return Values)

没有显式 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() 添加类型提示。

Any 类型

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 做字符串操作的类型检查。

玩转 Python 类型(第二部分)

让我们回到实践。你还记得之前尝试为通用的 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)

类型变量(Type Variables)

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

我们来为 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类型检查的资料请关注脚本之家其它相关文章!

相关文章

最新评论