Python 3 现代类型标注入门

Python 3 现代类型标注入门

面向 Python 初学者和 LeetCode 使用场景。本文主要采用 Python 3.10+ 的现代写法。

1. 类型标注是什么?

类型标注(type hint)是写在代码中的"类型说明"。它能告诉读代码的人、IDE 和类型检查工具:某个变量预计保存什么类型的数据,某个函数预计接收什么类型的参数、返回什么类型的结果。

python 复制代码
name: str = "Alice"
age: int = 18

def add(a: int, b: int) -> int:
    return a + b

上面的代码表达了:

  • name 预计是字符串;
  • age 预计是整数;
  • add() 的两个参数预计是整数;
  • add() 预计返回整数。

类型标注的主要价值:

  1. 提高代码可读性;
  2. 帮助 IDE 提供自动补全和错误提醒;
  3. 帮助类型检查工具提前发现问题;
  4. 让函数的输入和输出更清晰;
  5. 修改大型项目时更安全。

2. 最重要的一点:标注不等于强制检查

Python 仍然是一门动态类型语言。通常情况下,类型标注不会在程序运行时阻止错误类型的数据进入变量或函数。

python 复制代码
age: int = "十八岁"
print(age)  # Python 仍然可以运行到这里

下面的函数标明参数应为 int,但 Python 不会仅凭标注自动拦截字符串:

python 复制代码
def double(number: int) -> int:
    return number * 2

print(double("ha"))  # 输出 haha

因此,类型标注更像"说明书",不是运行时的"门卫"。IDE、Pyright、mypy 等工具可以根据说明书检查代码。

3. 变量的类型标注

基本语法:

python 复制代码
变量名: 类型 = 值

常见基础类型:

python 复制代码
name: str = "Alice"
age: int = 18
height: float = 1.68
is_student: bool = True
nothing: None = None

类型非常明显时,可以省略标注:

python 复制代码
name = "Alice"
age = 18

空容器的元素类型不明显,这时标注尤其有价值:

python 复制代码
names: list[str] = []
scores: dict[str, int] = {}
visited: set[int] = set()

也可以先声明,稍后再赋值:

python 复制代码
result: str

if age >= 18:
    result = "成年"
else:
    result = "未成年"

4. list、list[str] 和 [] 的区别

这是初学阶段最容易混淆的地方。

python 复制代码
list         # Python 内置的列表类
list[str]    # 类型标注:元素预计都是 str 的列表
[]           # 真正创建一个空列表
list()       # 也会真正创建一个空列表

正确写法:

python 复制代码
names: list[str] = []

可以拆开理解:

python 复制代码
names: list[str]  # 说明 names 预计是字符串列表
= []              # 创建空列表并赋给 names

下面的写法不是创建列表:

python 复制代码
names = list[str]  # names 得到的是一个类型表达式,不是空列表

因此不能把 list[str] 当成 [] 使用。

5. 常见容器的标注

5.1 列表 list

python 复制代码
names: list[str] = ["Alice", "Bob"]
numbers: list[int] = [1, 2, 3]
prices: list[float] = [9.9, 12.5]

list 通常只写一个元素类型:

python 复制代码
values: list[int] = [1, 2, 3]

不要写成:

python 复制代码
values: list[int, str] = []  # 错误:list 只接受一个元素类型参数

如果列表允许保存整数或字符串,可以使用联合类型:

python 复制代码
values: list[int | str] = [1, "hello", 2]

5.2 集合 set

python 复制代码
tags: set[str] = {"Python", "LeetCode"}
visited: set[int] = set()

注意:{} 创建的是空字典,空集合要用 set()。

5.3 字典 dict

python 复制代码
scores: dict[str, int] = {
    "Alice": 95,
    "Bob": 88,
}

dict[str, int] 中:

  • 第一个类型 str 是键的类型;
  • 第二个类型 int 是值的类型。

更多例子:

python 复制代码
id_to_name: dict[int, str] = {1: "Alice", 2: "Bob"}
word_count: dict[str, int] = {}

5.4 元组 tuple

元组通常分别标明每个位置的类型:

python 复制代码
point: tuple[int, int] = (10, 20)
person: tuple[str, int] = ("Alice", 18)

如果元组包含任意数量、类型相同的元素,使用 ...:

python 复制代码
numbers: tuple[int, ...] = (1, 2, 3, 4)

注意:

python 复制代码
value: tuple[int] = (1,)       # 恰好一个整数
values: tuple[int, ...] = ()   # 任意数量的整数,也可以为空

5.5 嵌套容器

类型标注可以嵌套:

python 复制代码
matrix: list[list[int]] = [
    [1, 2, 3],
    [4, 5, 6],
]

students: list[dict[str, int | str]] = [
    {"name": "Alice", "score": 95},
    {"name": "Bob", "score": 88},
]

嵌套层数很多时,类型可能会变得难读,可以考虑使用类型别名或类来表达数据结构。

6. 函数的类型标注

基本语法:

python 复制代码
def 函数名(参数名: 参数类型) -> 返回值类型:
    ...

例如:

python 复制代码
def add(a: int, b: int) -> int:
    return a + b

接收字符串列表并返回字符串:

python 复制代码
def first_word(words: list[str]) -> str:
    return words[0]

没有返回值的函数使用 -> None:

python 复制代码
def greet(name: str) -> None:
    print(f"你好,{name}")

这里的"没有返回值"是指没有返回有意义的数据。函数实际仍会返回 None。

6.1 默认参数

python 复制代码
def greet(name: str = "访客") -> str:
    return f"你好,{name}"

参数有默认值,不代表它必须写成 str | None。只有确实允许传入 None 时,才需要把 None 写进类型:

python 复制代码
def greet(name: str | None = None) -> str:
    if name is None:
        return "你好,访客"
    return f"你好,{name}"

6.2 *args 和 **kwargs

python 复制代码
def total(*numbers: int) -> int:
    return sum(numbers)

numbers: int 表示传给 *numbers 的每个参数都预计是整数。在函数内部,numbers 是 tuple[int, ...]。

python 复制代码
def show_scores(**scores: int) -> None:
    for name, score in scores.items():
        print(name, score)

scores: int 表示 **scores 中每个值都预计是整数;键由关键字参数决定,是字符串。

7. 联合类型:一个值允许多种类型

Python 3.10+ 推荐使用 |:

python 复制代码
def show(value: int | str) -> None:
    print(value)

这表示 value 可以是 int,也可以是 str。

可能是某种类型,也可能是 None:

python 复制代码
nickname: str | None = None

旧式写法如下:

python 复制代码
from typing import Optional, Union

value: Union[int, str]
nickname: Optional[str]

现代 Python 中优先写成:

python 复制代码
value: int | str
nickname: str | None

8. 类中的类型标注

python 复制代码
class Student:
    school: str = "第一中学"  # 类属性

    def __init__(self, name: str, age: int) -> None:
        self.name: str = name
        self.age: int = age

    def introduce(self) -> str:
        return f"我是{self.name},今年{self.age}岁"

通常不需要给 self 标注类型:

python 复制代码
def introduce(self) -> str:
    ...

IDE 和类型检查工具可以知道 self 是当前类的实例。

9. 循环变量如何标注?

不能直接在 for 语句的变量后写类型:

python 复制代码
# 错误语法
for number: int in numbers:
    print(number)

一般只需标注被遍历的容器:

python 复制代码
numbers: list[int] = [1, 2, 3]

for number in numbers:
    print(number)

类型检查工具会推断 number 是 int。

10. 什么时候应该写类型标注?

推荐标注:

  • 函数参数;
  • 函数返回值;
  • 空列表、空字典和空集合;
  • 类的属性;
  • 类型不明显的变量;
  • 会被其他模块使用的公开函数。

类型很明显的简单局部变量,可以省略:

python 复制代码
name = "Alice"
count = 0
numbers = [1, 2, 3]

也可以标注,但不必为了标注而标注:

python 复制代码
name: str = "Alice"
count: int = 0
numbers: list[int] = [1, 2, 3]

好的类型标注应该帮助阅读,而不是制造视觉负担。

11. 类型推断

即使没有显式标注,IDE 和类型检查工具也经常可以根据赋值推断类型:

python 复制代码
name = "Alice"       # 通常可推断为 str
age = 18             # 通常可推断为 int
numbers = [1, 2, 3]  # 通常可推断为 list[int]

空列表中没有元素,无法仅根据 [] 看出未来要保存什么,因此建议标注:

python 复制代码
words: list[str] = []

12. typing.List 和内置 list 的区别

旧式写法:

python 复制代码
from typing import List

names: List[str] = []

Python 3.9+ 的现代写法:

python 复制代码
names: list[str] = []

它们在类型标注中表达的意思基本相同,但 typing.List 和内置的 list 不是同一个对象。现代项目通常优先使用内置类型:

python 复制代码
list[str]
dict[str, int]
set[str]
tuple[int, int]

这不代表 typing 模块完全没用了。一些更高级的类型工具仍然需要从 typing 导入,例如 Any、Literal、TypedDict、Protocol 等。

13. 类型别名

复杂类型多次出现时,可以起一个更容易阅读的名字:

python 复制代码
Matrix = list[list[int]]

def rotate(matrix: Matrix) -> Matrix:
    return [list(row) for row in zip(*matrix[::-1])]

Python 3.12+ 还支持更明确的 type 语句:

python 复制代码
type Matrix = list[list[int]]

如果 LeetCode 的 Python 版本不确定,普通赋值形式通常兼容性更好:

python 复制代码
Matrix = list[list[int]]

14. 可调用对象(函数)的标注

需要描述"参数本身是一个函数"时,可以使用 Callable:

python 复制代码
from collections.abc import Callable

def calculate(
    a: int,
    b: int,
    operation: Callable[[int, int], int],
) -> int:
    return operation(a, b)

Callable[[int, int], int] 表示:

  • 这个对象可以像函数一样调用;
  • 接收两个 int;
  • 返回一个 int。

使用示例:

python 复制代码
def add(a: int, b: int) -> int:
    return a + b

answer = calculate(2, 3, add)

15. LeetCode 中的实际例子

最长公共前缀:

python 复制代码
class Solution:
    def longestCommonPrefix(self, strs: list[str]) -> str:
        if not strs:
            return ""

        for index, char in enumerate(strs[0]):
            for word in strs[1:]:
                if index >= len(word) or word[index] != char:
                    return strs[0][:index]

        return strs[0]

函数签名:

python 复制代码
def longestCommonPrefix(self, strs: list[str]) -> str:

表示:

  • self 是当前 Solution 实例,通常不用手动标注;
  • strs 预计是字符串列表;
  • 函数预计返回一个字符串。

调用时:

python 复制代码
words: list[str] = ["flower", "flow", "flight"]
answer: str = Solution().longestCommonPrefix(words)
print(answer)  # fl

16. 常见错误

错误 1:把类型标注当成创建对象

python 复制代码
words = list[str]  # 没有创建空列表

正确:

python 复制代码
words: list[str] = []

错误 2:认为标注会自动检查类型

python 复制代码
age: int = "18"  # Python 通常不会因为标注而立即报错

类型检查需要 IDE 或 Pyright、mypy 等工具协助。

错误 3:忘记标注函数返回值

python 复制代码
def add(a: int, b: int):
    return a + b

这段代码能运行,但接口信息不完整。推荐:

python 复制代码
def add(a: int, b: int) -> int:
    return a + b

错误 4:把默认参数与允许 None 混为一谈

python 复制代码
def greet(name: str = "访客") -> str:
    ...

这里 name 有默认值,但仍然只预计接收字符串。只有真的允许 None 时才写:

python 复制代码
def greet(name: str | None = None) -> str:
    ...

错误 5:为可变参数设置可变默认值

这不是类型标注本身的问题,但经常一起出现:

python 复制代码
# 不推荐
def add_name(name: str, names: list[str] = []) -> list[str]:
    names.append(name)
    return names

推荐使用 None:

python 复制代码
def add_name(
    name: str,
    names: list[str] | None = None,
) -> list[str]:
    if names is None:
        names = []

    names.append(name)
    return names

17. Python 版本提示

本文采用的两类现代语法出现于不同版本:

python 复制代码
list[str]       # Python 3.9+
int | str       # Python 3.10+
type UserId = int  # Python 3.12+

如果统一使用 Python 3.10+,可以放心使用本文绝大多数示例。LeetCode 提供的默认函数签名如果已经使用 list[str],直接沿用即可。

18. 速查表

需求 现代类型标注
整数 int
浮点数 float
字符串 str
布尔值 bool
无返回值 None
字符串列表 list[str]
整数集合 set[int]
字符串到整数的字典 dict[str, int]
两个整数组成的元组 tuple[int, int]
任意数量的整数元组 tuple[int, ...]
整数或字符串 `int
字符串或空值 `str
二维整数列表 list[list[int]]
接收两个整数并返回整数的函数 Callable[[int, int], int]

19. 推荐的学习顺序

  1. 先熟悉变量、函数参数和返回值的标注;
  2. 掌握 list、dict、set、tuple 的标注;
  3. 掌握 | 和 None;
  4. 在 LeetCode 函数签名中读懂输入、输出类型;
  5. 再学习 Callable、类型别名和 typing 中的高级工具;
  6. 项目变大后,再配合 Pyright 或 mypy 做静态检查。

20. 一句话总结

python 复制代码
words: list[str] = []

可以理解为:

使用 [] 创建一个真正的空列表,并用 list[str] 告诉读者和工具:这个列表预计只保存字符串。

类型标注的目标不是让代码看起来更复杂,而是让数据的含义、函数的输入和输出一眼就能看懂。

参考资料

相关推荐
计算机编程指导师1 小时前
大数据毕业设计推荐:Hadoop+Django全球碳排放减排策略分析与可视化系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·hadoop·python·数据分析·spark·毕业设计·碳排放
计算机编程指导师2 小时前
Python大数据毕设怎么做?基于Hadoop+Spark的高频电力消耗数据可视化与分析系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·hadoop·python·spark·毕业设计·课程设计·电力
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
c++11:fuction包装器
开发语言·c++
kimnoic2 小时前
Python 爬虫的原理
开发语言·python
Fluxproxy2 小时前
Python爬虫异步提速终极方案|线程池ThreadPoolExecutor实战,告别单线程慢速爬取
开发语言·爬虫·python
蜗牛互联网2 小时前
Java标准HTTP调用多模态模型的票据抽取前置校验
java·人工智能·python·http
用户019027581612 小时前
AlphaFeed K 线的 OHLCV 字段怎么理解?(含单位)
python
淡海水2 小时前
02-07-原理篇-GC暂停时间的本质
开发语言·unity·c#·gc·暂停时间
高级程序源2 小时前
SSM仓储管理系统31582-计算机课程设计、毕业设计
vue.js·python·mysql·小程序·flask·课程设计