python函数Optional和Union注解怎么用
详细讲解Python中typing.Optional与typing.Union注解的使用方法、核心差异、容易混淆的默认值误区以及Python 3.10以后的新语法和最佳实践。
在 Python 的类型注解体系中,Union 与 Optional 都来自内置模块 typing。简单来说,Union 用于表示一个值可以是多种指定类型中的任意一种(如既能接收整数也能接收字符串),而 Optional 则是专门用来表示“某个具体类型或为 None”的简写形式。本质上,Optional[T] 完全等同于 Union[T, None]。理解这两个注解不仅能显著提升代码的可读性,更能配合静态检查工具(如 mypy、Pyright)在代码运行前提前排查潜在的空指针或类型不匹配隐患。
一、核心概念:什么是 Union 与 Optional?
在编写 Python 函数时,很多场景下函数的参数或返回值并不局限于单一数据类型:
- 一个查询接口可能既允许传入数值型的主键
id: int,也允许传入字符串格式的编码id: str; - 一个从缓存或数据库获取数据的函数,在命中时返回对应的数据对象,但在未命中或发生特定异常时会返回
None。
为了在类型签名中清晰表达这种“多选一”以及“可为空”的逻辑,Python 在 3.5 引入的 typing 模块中提供了 Union 和 Optional。
1. typing.Union 的本质
Union[X, Y, ...] 表达的是集合论中的并集关系。凡是被 Union[X, Y] 注解的变量,代表它在运行时的合法类型既可以是 X,也可以是 Y。如果传入或返回其他类型,静态类型检查工具就会标记错误。
2. typing.Optional 的本质
很多初学者容易将 Optional 误认为“这个参数在调用函数时是可选的(可以不传)”,这是一个严重的认知偏差。在 Python 类型系统中,Optional[T] 的标准定义是:Union[T, None] 的简写糖衣。它仅仅表示该变量的值可以是 T 类型,也可以是 None,并不代表调用时可以省略该参数。
二、Union 的典型使用场景与代码示例
Union 适合用于函数参数支持多种输入格式、或者函数根据不同输入可能产生多种不同返回值类型的场景。
1. 参数支持多类型输入
在处理文件路径或配置项时,常常允许调用者传入字符串或 pathlib.Path 对象;或者在查询用户时,既支持通过整型用户 ID 查询,也支持通过手机号/用户名字符串查询:
from typing import Union
from pathlib import Path
def load_configuration(source: Union[str, Path, dict]) -> dict:
"""接收文件路径字符串、Path对象或者直接接收字典配置"""
if isinstance(source, dict):
return source
path_obj = Path(source)
if not path_obj.exists():
raise FileNotFoundError(f"配置文件不存在: {path_obj}")
return {"loaded_from": str(path_obj)}
2. 函数返回多种可能的结果
有些数学运算或解析逻辑可能返回具体数值,在边界条件下返回特定状态码或异常对象:
from typing import Union
def parse_int_or_fail(raw_text: str) -> Union[int, ValueError]:
try:
return int(raw_text)
except ValueError as err:
return err
三、Optional 的典型使用场景与代码示例
只要函数在某种逻辑分支下可能显式返回 None,或者参数允许显式传入 None,就应该使用 Optional。
1. 查找类操作的返回值
在数据库查找或字典键值过滤中,若找不到目标实体,返回 None 是 Python 中最普遍的习惯用法:
from typing import Optional, Dict
class User:
def __init__(self, uid: int, name: str):
self.uid = uid
self.name = name
# 模拟用户数据库存储
USER_DB: Dict[int, User] = {
101: User(101, "Alice"),
102: User(102, "Bob"),
}
def query_user_by_id(uid: int) -> Optional[User]:
"""若找到用户则返回 User 实例,若不存在则返回 None"""
return USER_DB.get(uid, None)
通过标注 -> Optional[User],任何使用该函数结果的调用方,IDE 和静态类型检查工具都会立刻提醒:“返回值可能是 None,直接调用 .name 会有空指针异常风险!”这迫使开发者在使用前先进行判空处理。
2. 包含可选过滤条件的函数参数
如果函数允许调用者传入特定筛选条件,但在调用者不指定时不做任何筛选,此时参数既需要类型提示,也需要赋予默认值 None:
from typing import Optional, List
def fetch_logs(limit: int = 100, tag: Optional[str] = None) -> List[str]:
"""tag 允许传字符串进行标签筛选;若传 None 或缺省,则不按标签过滤"""
logs = ["auth: login", "db: connect", "api: request"]
if tag is not None:
return [item for item in logs if item.startswith(tag)]
return logs[:limit]
四、标准使用流程:从需求到类型标注的决策路径
给函数添加参数与返回值注解并不是随手写上类型,而是需要遵循清晰的思考路径。下图展示了给 Python 函数编写注解时的标准操作步骤:
五、重大避坑提醒:Optional[T] 绝不等于“参数可选”
这是 Python 初学者乃至部分中级开发者最常踩的误区:误将“参数类型允许为 None”与“参数可以不传(具有默认值)”混为一谈。
我们来看下面三个函数定义的巨大差异:
from typing import Optional
# 形式 A:只声明了类型允许为 None,但没有提供默认值!
def save_avatar(user_id: int, image_url: Optional[str]):
print(f"User: {user_id}, Avatar: {image_url}")
# 形式 B:既声明了类型允许为 None,又赋予了默认值 None
def save_profile(user_id: int, bio: Optional[str] = None):
print(f"User: {user_id}, Bio: {bio}")
# 形式 C:参数有默认值,但类型绝不允许为 None
def set_timeout(seconds: int = 30):
print(f"Timeout: {seconds}s")
当我们分别调用这些函数时:
- 对于
save_avatar(101),Python 会在运行时直接抛出TypeError: save_avatar() missing 1 required positional argument: 'image_url'!因为Optional[str]只表示你可以传入"http://..."或者显式传入None(如save_avatar(101, None)),但依然是必填参数。 - 对于
save_profile(101),这才是真正的“可选参数”,因为有= None作为默认值,缺省时自动使用None。 - 对于
set_timeout(),它是可选参数,但你如果传set_timeout(None),类型检查器就会立即报警,因为它的参数必须是整型,不允许赋空。
在较旧的 Python 版本(如 Python 3.5 早期),如果写了 val: str = None,mypy 等类型检查器曾默认隐式将其当成 Optional[str]。但在 PEP 484 更新以及后续类型检查器规范中,隐式 Optional 已被彻底废弃。若未声明 Optional[str] 或 str | None,给 str 参数赋 None 默认值会被现代类型检查器严厉警告。因此必须显式写出 Optional[str] = None。
六、接收复合类型后的安全处理:类型收窄(Type Narrowing)
如果函数入参标注了 Union[str, int] 或者 Optional[List[str]],在函数体内部就不能随意调用专属于某一类型的方法。例如,对 Optional[str] 直接调用 .strip() 会触发静态类型检查报错,因为 None 没有 strip 方法。
现代类型检查器非常智能,只要在代码中加入了分支判断,检查器就会自动在分支内部“收窄”变量类型:
from typing import Optional, Union
def process_identifier(data: Optional[Union[str, int]]) -> str:
# 守卫子句:收窄排除 None
if data is None:
return "EMPTY_ID"
# 此时类型被收窄为 Union[str, int]
if isinstance(data, int):
# 在此分支内,类型被精确收窄为 int
return f"NUM-{data:06d}"
# 在剩余分支内,类型自动收窄为 str
return f"STR-{data.upper()}"
七、Python 3.10+ 新语法:管道符 | 彻底取代 Union 与 Optional
随着 Python 3.10(PEP 604)的发布,Python 引入了原生类型并集运算符 |。这个语法极大地简化了类型注解的书写:
| 场景 | Python 3.5 - 3.9 写法 | Python 3.10+ 现代写法(PEP 604) |
|---|---|---|
| 多类型联合 | from typing import Union |
x: int | str |
| 允许为空 (Optional) | from typing import Optional |
x: str | None = None |
| 多类型且允许为空 | from typing import Union, Optional |
x: int | str | None = None |
| isinstance 运行时检查 | isinstance(x, (int, str)) |
isinstance(x, int | str)(3.10+ 支持) |
在低版本中提前使用管道符语法
如果项目运行在 Python 3.7+ 环境,但希望享受 Python 3.10 的整洁语法,只需在 Python 文件顶部加入一行导入:
from __future__ import annotations
# 在 3.7+ 环境下即可正常使用 | 语法作为类型注解
def calculate_hash(source: str | bytes | None = None) -> str:
if source is None:
return ""
if isinstance(source, str):
return f"hash({source})"
return f"bytes_hash({source.hex()})"
需要注意:from __future__ import annotations 会将类型注解延迟为字符串解析(PEP 563),因此可以正常书写 str | bytes。但对于 isinstance(source, str | bytes) 这种运行时语句,仍需 Python 3.10+ 解释器本身支持。
八、总结与选型决策指南
为了在实际开发中快速选用最合适的注解方式,可以参考以下决策速查图与核心原则:
通过合理运用 Union 与 Optional(或 3.10+ 的 | 语法),不仅可以建立起严格、自解释的接口协议,还能让编辑器智能提示与代码静态检查工具充分发挥效能,在编码阶段就杜绝因传入不合法数据类型或忽略空指针检查而产生的潜在 Bug。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









