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()预计返回整数。
类型标注的主要价值:
- 提高代码可读性;
- 帮助 IDE 提供自动补全和错误提醒;
- 帮助类型检查工具提前发现问题;
- 让函数的输入和输出更清晰;
- 修改大型项目时更安全。
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. 推荐的学习顺序
- 先熟悉变量、函数参数和返回值的标注;
- 掌握
list、dict、set、tuple的标注; - 掌握
|和None; - 在 LeetCode 函数签名中读懂输入、输出类型;
- 再学习
Callable、类型别名和typing中的高级工具; - 项目变大后,再配合 Pyright 或 mypy 做静态检查。
20. 一句话总结
python
words: list[str] = []
可以理解为:
使用
[]创建一个真正的空列表,并用list[str]告诉读者和工具:这个列表预计只保存字符串。
类型标注的目标不是让代码看起来更复杂,而是让数据的含义、函数的输入和输出一眼就能看懂。