☰
Python核心语法(五):类型注解
2026/10/10 12:18:25 网站建设 项目流程

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))

输出结果:

张三 None

Python 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 的旧写法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询