Python核心语法(五):类型注解
前言
前五篇我们梳理了数据存储与运算、流程控制语句、数据容器、函数、正则表达式,程序已经能存数据、做判断、管理集合、封装代码、处理文本了。但你有没有遇到这种情况:写函数的时候,参数名叫 data,写的时候记得它是字符串,过两周再打开——诶?这是字符串还是列表来着?只能翻函数内部一行行看。还有同事调用你写的函数,把整数传给了只该收字符串的参数,程序跑起来没报错,结果输出一堆乱码,查了半天。
这一篇整理类型注解——给变量、参数、返回值"贴标签"的语法。它不改变代码的运行方式,却能让代码自己"说话":一眼看清每个数据是什么类型。而且这是当下 Python 圈的"大势所趋":FastAPI、Pydantic 这些热门框架靠它实现自动校验,AI 编程助手(Copilot 等)靠它猜得更准,大厂开源项目几乎全部在用。
类型注解主要分为四大块:
- 变量与容器的类型注解:给普通变量、列表、字典、集合"贴标签"
- 函数的类型注解:参数类型、返回值类型,把函数"说明书"写进代码
- typing 模块与常用类型:Optional、Any、Union、Callable……应对更复杂的场景
- 静态检查工具:让"贴标签"从摆设变成真正能查错的利器
一、类型注解是什么
1.生活中的类比
你收到一个快递箱,上面只写着"东西",你会发懵:这是书?是杯子?还是易碎的玻璃?但如果贴着标签——“内容物:玻璃杯,易碎,向上”——你立刻知道怎么对待它。类型注解就是贴在变量上的"快递标签"。
类型注解(Type Annotation,也叫类型提示 Type Hint):写在代码里、用来说明"这个数据是什么类型"的标注语法。它只负责"声明",不负责"拦截"——就像快递标签能提醒搬运工小心,但没法阻止搬运工暴力抛掷。
2.最基本的语法
变量注解:在变量名后面加冒号,写上类型。
name:str="张三"age:int=20height:float=1.75is_vip:bool=True函数注解:参数类型写在冒号后面,返回值类型写在->后面。
defadd(a:int,b:int)->int:returna+b读作:add 函数收两个整数,返回一个整数。是不是有点像数学里的函数定义 f(x: 整数) → 整数?
*注意:类型注解不改变运行结果。写 name: str = “张三” 和 name = “张三” 跑起来一模一样,注解只是给人看和给工具看的"说明书"。
二、变量与容器的类型注解
1.普通变量
name:str="张三"age:int=20score:float=88.5is_pass:bool=True输出结果:
(没有输出,这只是"声明+赋值",注解不产生任何输出)*注意:变量注解可以只声明不赋值:x: int 这一行完全合法,此时 x 还不存在(没绑定值),但类型已经登记了。适合先声明后面再赋值的场景。
2.容器注解要写"内容物类型"
只写 list 太笼统——列表里装的是整数还是字符串?要用方括号写清楚"内容物":
# 列表:装 int 的列表scores:list[int]=[90,85,78]# 元组:装 str 的元组cities:tuple[str,...]=("北京","上海","深圳")# 字典:键是 str,值是 int(两种类型用逗号隔开)id_map:dict[str,int]={"张三":1,"李四":2}# 集合:装 float 的集合temps:set[float]={36.5,37.0}输出结果:
(同样没有输出,纯注解声明)*注意:元组的特殊写法:固定长度的元组写 tuple[int, str](第一个是 int、第二个是 str),长度不定的写 tuple[int, …](三个点表示"后面全是 int,数量不限")。字典 dict[键类型, 值类型] 两个都要写,只写一个报 TypeError。
3.3.9 之前的老写法(看懂旧代码)
Python 3.9 之前,list[int] 这种写法会报错,必须从 typing 模块导入大写开头的类型:
fromtypingimportList,Dict,Tuple,Set scores:List[int]=[90,85,78]id_map:Dict[str,int]={"张三":1}现在写代码一律用小写的 list[int]、dict[str, int] 就好,但读别人(尤其网上)的旧代码、旧教程时,要认得 List、Dict 这些大写形式是一个意思。
三、函数的类型注解
1.给参数和返回值"贴标签"
defadd(a:int,b:int)->int:returna+bdefgreet(name:str)->str:return"你好,"+namedeflog_message(msg:str)->None:print(f"[日志]{msg}")print(add(3,5))print(greet("张三"))log_message("服务启动")输出结果:
8 你好,张三 [日志] 服务启动拆解一下 greet(“张三”) 的注解:
- name: str → 参数 name 是字符串
- -> str → 返回值是字符串
*注意:没有返回值的函数返回值类型写 -> None。像 log_message 这种只打印不 return 的函数,写 -> None(也可以省略不写,但规范做法是写上)。
2.注解错了也不报错(重点!)
defdouble(n:int)->int:returnn*2result=double("hi")print(result)输出结果:
hihi明明注解说参数是 int,传了个字符串 “hi”,程序不但不报错,还正常运行输出 “hihi”(字符串乘法,重复两遍)!
*注意:这是类型注解最颠覆新手认知的一点:Python 运行时不会检查类型注解!注解错了照样跑,直到哪天逻辑炸了才暴露。它是"说明书",不是"安检门"。那注解的意义在哪?一是给人看(IDE 会提示、报错警告),二是给工具看(mypy 等静态检查工具,见第六章)。
3.默认值与注解同时存在
写法顺序是"参数名: 类型 = 默认值",冒号在前、等号在后:
defintroduce(name:str,age:int=18)->str:returnf"我叫{name},今年{age}岁"print(introduce("张三"))print(introduce("李四",22))输出结果:
我叫张三,今年18岁 我叫李四,今年22岁*注意:默认值是 None 时,类型要写 str | None(或 Optional[str],见第四章),不能写 str = None——注解说"必须是字符串"却给个 None,工具检查时算自相矛盾。
# 错误示范(mypy 会警告):deffind(name:str=None)->str:...# 正确写法:deffind(name:str|None=None)->str|None:...4.可变类型默认值的大坑
第四章讲过:默认值用可变对象(列表、字典)会被所有调用共享。类型注解也一样:
defappend_item(item:str,my_list:list[str]=[])->list[str]:my_list.append(item)returnmy_listprint(append_item("苹果"))print(append_item("香蕉"))输出结果:
['苹果'] ['苹果', '香蕉']第二次调用把"苹果"也带上了——因为默认的 [] 是同一个列表,一直在被追加。正确做法是用 None 占位:
defappend_item(item:str,my_list:list[str]|None=None)->list[str]:ifmy_listisNone:my_list=[]my_list.append(item)returnmy_listprint(append_item("苹果"))print(append_item("香蕉"))输出结果:
['苹果'] ['香蕉']四、typing 模块与常用类型
简单类型用 int、str 就够了,但真实业务远比这复杂:参数可能是字符串也可能是 None、可能是多种类型之一、可能是另一个函数……这些靠 typing 模块(和 Python 3.10 后的新语法)解决。
1.Optional:可能有,也可能没有
Optional[X]:表示"X 类型,或者 None",专治"查不到就返回 None"的场景。
fromtypingimportOptionaldeffind_user(user_id:int)->Optional[str]:users={1:"张三",2:"李四"}returnusers.get(user_id)# get 查不到时返回 Noneprint(find_user(1))print(find_user(99))输出结果:
张三 NonePython 3.10+ 的新写法是直接用竖线:
deffind_user(user_id:int)->str|None:users={1:"张三",2:"李四"}returnusers.get(user_id)*注意:Optional[str] 和 str | None 是完全等价的两种写法,新版直接用竖线更简洁。竖线是 Python 3.10 才支持的语法,如果项目要兼容更老的版本,就得用 Optional。
2.Union:多选一
Union[X, Y]:表示"X 或 Y 都行",适合"既能传单个值又能传列表"这类灵活参数。
fromtypingimportUniondefshow_id(user_id:Union[int,str])->None:print(f"用户ID:{user_id}")show_id(1001)show_id("A1001")输出结果:
用户ID:1001 用户ID:A1001新写法:Union[int, str]等价于int | str,竖线可以连着写多个:int | str | None。
*注意:Union[X, Y] 里写了 None 等价于 X | Y | None,所以 Union[str, None] 就是 Optional[str],俩工具可以互相替换,别被不同写法迷惑。
3.Any:万金油
Any:表示"任意类型都行",相当于没有注解。适合暂时不想约束、或者写得太杂懒得列全的场景。
fromtypingimportAnydefprint_anything(data:Any)->None:print(f"收到:{data}")print_anything(123)print_anything("文本")print_anything([1,2,3])输出结果:
收到:123 收到:文本 收到:[1, 2, 3]*注意:Any 是"逃生舱口"而不是"免死金牌"。用 Any 的地方工具就不会帮你检查了,等于放弃保护。能写具体类型就写具体类型,Any 只在确实无法确定时使用。
4.函数也能当参数
函数名本身就是变量,当然也有类型。用 Callable 注解"参数是一个函数":
fromtypingimportCallabledefapply_twice(func:Callable[[int],int],value:int)->int:returnfunc(func(value))defadd_one(n:int)->int:returnn+1print(apply_twice(add_one,5))输出结果:
7*注意:Callable[[int], int] 的两个方括号含义:里面第一个 [int] 是参数类型列表(多个参数逗号隔开),外面那个 int 是返回值类型。读作"接收一个 int 参数、返回 int 的函数"。
五、给自定义类做注解
类型注解不只是内置类型的专利,自己写的类同样可以(也是框架里最常见的用法):
classStudent:def__init__(self,name:str,age:int)->None:self.name=name self.age=agedefprint_student(stu:Student)->None:print(f"姓名:{stu.name},年龄:{stu.age}")s=Student("张三",20)print_student(s)输出结果:
姓名:张三,年龄:20*注意:引用还没定义的类(比如 A 类的方法返回 A 自己,或两个类互相引用)时,类型名要加引号写成 “A”,这叫"前向引用"。Python 3.14 起默认自动处理这种场景不用加引号,但看旧代码时要认得,而且 IDE 里的 Pylance、mypy 对没加引号的情况也可能提前告警。
六、静态检查工具:让标签真正管用
写到这儿你可能想:既然注解运行时不检查,那不就是个"好看的注释"吗?——单独看确实是。但配合静态检查工具,它就从"说明书"升级成"安检门"。
1.mypy:最主流的类型检查工具
mypy 的逻辑是:不运行代码,光"读"代码,拿工具眼里"实际类型"和注解"声明类型"做对比,对不上就报错。安装:
pip install mypy准备一个故意出错的文件 demo.py:
defdouble(n:int)->int:returnn*2result=double("hi")print(result)在命令行运行检查(不运行代码):
mypy demo.py输出结果:
demo.py:4: error: Argument 1 to "double" has incompatible type "str"; expected "int" [arg-type] Found 1 error in 1 file (checked 1 source file)第 4 行直接被揪出来:你声明参数是 int,却传了个 str。同一个文件直接用 python 运行会输出 hihi 什么都不报,mypy 一查一个准。
*注意:mypy 报错的文件名和行号可能因运行环境略有差异(比如显示相对路径、多一个方括号错误码),属正常现象,关注"第几行、什么错"即可。
2.IDE 提示:写代码时就受益
其实不装 mypy,你平时用的编辑器(VS Code + Pylance、PyCharm)读了类型注解后就会:
- 调用函数时弹参数提示:鼠标悬停 add 函数,显示 (a: int, b: int) -> int
- 传错类型时画波浪线警告
- 打点号时智能补全,注解越准补全越准
这也是为什么说 AI 编程助手依赖类型注解——提示越明确,AI 猜得越准,生成的代码质量越高。
3.实践的度:别走火入魔
*注意:类型注解是"渐进式"的,Python 官方的设计哲学就是不强制。可以从"只给函数签名加注解"开始,收益最大、成本最小;变量类型一眼能看出来的(name = “张三”)就不用注解。不要为了注解把代码写得密密麻麻,反而伤害可读性。
七、易混淆知识点对比
1.类型注解 vs 运行时强制检查(最高频误区!)
| 对比项 | 类型注解 |
|---|---|
| 作用 | 声明"应该是什么类型" |
| 运行时是否检查 | 否,传错类型照样跑 |
| 谁来检查 | IDE 警告、mypy 等静态工具 |
2.list[int] vs typing.List[int]
| 对比项 | list[int] | List[int] |
|---|---|---|
| 可用版本 | Python 3.9+ | Python 3.5+(现在仍可用,属旧写法) |
| 写法来源 | 内置类型直接用 | 从 typing 模块导入 |
| 现在推荐 | 是 | 仅用于看懂旧代码 |
3.Optional[str] vs str | None
| 对比项 | Optional[str] | str | None |
|---|---|---|
| 可用版本 | Python 3.5+ | Python 3.10+ |
| 等价性 | 完全等价 | 完全等价 |
| 新代码推荐 | 看团队规范 | 是(更简洁) |
4.注解写在哪
| 场景 | 写法 |
|---|---|
| 变量 | name: str = “张三” |
| 函数参数 | def f(a: int) |
| 函数返回值 | def f(a: int) -> str |
| 默认值参数 | def f(a: int = 0) |
| 默认为空 | def f(a: int | None = None) |
5.Any vs 不写注解
| 对比项 | Any | 不写 |
|---|---|---|
| 态度 | 明确声明"任意类型" | 没声明 |
| 工具态度 | 检查时跳过该处 | 按规则推断 |
| 语义 | “我故意的” | “我忘了/懒得写” |
八、小结
- 类型注解是"快递标签":变量名: 类型、参数: 类型、-> 返回值类型,只声明、不拦截,运行时不改变任何行为。
- 变量注解:name: str = “张三”;容器要写内容物:list[int]、dict[str, int]、tuple[str, …]、set[float]。
- 函数注解:def add(a: int, b: int) -> int;无返回值写 -> None;默认值写法是 参数名: 类型 = 默认值。
- Optional[str] = str | None(可能为空),Union[int, str] = int | str(多选一),Any 是放弃检查的"万金油",Callable[[int], int] 表示"收 int 返回 int 的函数"。
- 可变默认值大坑:list、dict 做默认值会被共享,用 None 占位 + 函数内新建。
- 运行时不检查≠没用:mypy 静态检查能把"声明"与"实际"对不上的地方揪出来,IDE 靠注解给提示和补全,AI 编程助手靠它猜得更准。
- 渐进式使用:先给函数签名加注解(收益最大),变量一眼能看懂的不用注解;老代码里 typing.List 是 list 的旧写法。