Python 类型注解
类型注解(Type Hints)是 Python 3.5 引入的功能,可以为变量、函数参数和返回值添加类型信息。这是一种可选的静态类型系统,运行时不强制执行类型检查,但可以被类型检查器(如 mypy)、IDE 和 linter 使用,帮助开发者在编码阶段发现潜在的类型错误。
为什么使用类型注解?
提高代码可读性
类型注解明确表达了函数的输入输出类型,使代码意图更加清晰:
# 没有类型注解 - 参数和返回值类型不明确
def calculate_discount(price, rate):
return price * rate
# 有类型注解 - 一目了然
def calculate_discount(price: float, rate: float) -> float:
return price * rate
静态类型检查
配合 mypy、pyright 等工具,可以在编码阶段发现类型错误,而不是等到运行时才暴露问题:
def greet(name: str) -> str:
return f"Hello, {name}!"
# 类型检查器会报错
result: int = greet("Alice") # Error: 返回值是 str,不是 int
greet(123) # Error: 参数应该是 str,不是 int
IDE 支持
类型注解让 IDE 能够提供更准确的代码补全、参数提示和错误标记:
def process_data(data: list[str]) -> dict[str, int]:
result = {}
for item in data:
# IDE 知道 item 是 str,提供字符串方法补全
result[item.lower()] = len(item)
return result
文档作用
类型注解本身就是很好的文档,减少了写额外注释的需要:
def fetch_user(
user_id: int,
include_deleted: bool = False
) -> dict[str, str | int] | None:
"""获取用户信息。"""
...
基本类型注解
变量注解
# 基本类型
name: str = "张三"
age: int = 25
price: float = 19.99
is_active: bool = True
# 容器类型(Python 3.9+)
numbers: list[int] = [1, 2, 3]
scores: dict[str, int] = {"Alice": 90, "Bob": 85}
unique_ids: set[int] = {1, 2, 3}
# 兼容旧版本的写法(Python 3.5-3.8)
from typing import List, Dict, Set
numbers: List[int] = [1, 2, 3]
scores: Dict[str, int] = {"Alice": 90}
unique_ids: Set[int] = {1, 2, 3}
函数注解
# 基本函数注解
def greet(name: str) -> str:
return f"Hello, {name}!"
# 多个参数
def add(a: int, b: int) -> int:
return a + b
# 无返回值
def log_message(message: str) -> None:
print(message)
# 默认参数
def create_user(name: str, age: int = 0) -> dict[str, str | int]:
return {"name": name, "age": age}
类属性注解
class Person:
# 类属性
species: str = "人类"
# 实例属性(在 __init__ 中赋值)
name: str
age: int
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def introduce(self) -> str:
return f"我是{self.name},今年{self.age}岁"
# 使用 dataclass 简化
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
grade: str = "大一"
类型别名
类型别名用于简化复杂的类型声明,让代码更易读。
type 语句(Python 3.12+)
Python 3.12 引入了 type 语句来声明类型别名:
# 简单类型别名
type Point = tuple[float, float]
type UserId = int
type JsonValue = str | int | float | bool | None | list['JsonValue'] | dict[str, 'JsonValue']
# 使用类型别名
def distance(p1: Point, p2: Point) -> float:
return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5
# 泛型类型别名
type Vector[T] = list[tuple[T, T]]
type Result[T, E] = tuple[T, None] | tuple[None, E]
coordinates: Vector[float] = [(1.0, 2.0), (3.0, 4.0)]
传统类型别名(Python 3.5+)
from typing import TypeAlias, Union
# 简单赋值
Point = tuple[float, float]
# 显式标注(Python 3.10+)
UserId: TypeAlias = int
JsonValue: TypeAlias = Union[
str, int, float, bool, None,
list['JsonValue'], dict[str, 'JsonValue']
]
NewType:创建区分类型
NewType 用于创建在类型层面区分、但运行时实际上是原类型的"新类型"。这在需要避免混淆相似类型时非常有用。
from typing import NewType
# 创建区分类型
UserId = NewType('UserId', int)
ProductId = NewType('ProductId', int)
def get_user(user_id: UserId) -> str:
return f"User {user_id}"
def get_product(product_id: ProductId) -> str:
return f"Product {product_id}"
# 正确使用
user_id = UserId(12345)
product_id = ProductId(67890)
get_user(user_id) # OK
get_product(product_id) # OK
# 类型错误 - 不能混淆
get_user(product_id) # Error: 期望 UserId,实际是 ProductId
get_product(user_id) # Error: 期望 ProductId,实际是 UserId
# 运行时仍然是 int
print(type(user_id)) # <class 'int'>
print(user_id + 1) # 12346 - 可以进行 int 运算
应用场景:
- 区分不同含义的 ID(用户 ID、订单 ID、产品 ID)
- 区分不同单位的数值(米、秒、像素)
- 防止参数传递错误
from typing import NewType
Seconds = NewType('Seconds', float)
Milliseconds = NewType('Milliseconds', float)
def wait(duration: Seconds) -> None:
print(f"等待 {duration} 秒")
# 类型检查器会警告
wait(Milliseconds(500)) # Error: 类型不匹配
联合类型
Union 类型
Union 表示值可能是多种类型之一:
from typing import Union
def process(value: Union[int, str, float]) -> str:
return str(value)
# Python 3.10+ 可以使用 | 语法(推荐)
def process(value: int | str | float) -> str:
return str(value)
Optional 类型
Optional[T] 等价于 T | None,表示值可能为 None:
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
if user_id > 0:
return "张三"
return None
# Python 3.10+ 推荐写法
def find_user(user_id: int) -> str | None:
if user_id > 0:
return "张三"
return None
# 使用时需要检查 None
user = find_user(1)
if user is not None:
print(user.upper()) # 安全:user 确定不是 None
None 如何影响类型推断
def get_length(items: list[str] | None) -> int:
# 类型检查器知道这里 items 可能是 None
if items is None:
return 0
# 类型收窄:这里 items 确定是 list[str]
return len(items)
特殊类型
Any 类型
Any 表示任意类型,关闭类型检查。它是类型的"逃生舱",但应谨慎使用:
from typing import Any
def log_value(value: Any) -> None:
# 可以对 Any 类型进行任何操作
print(value)
print(value.any_method()) # 类型检查器不会报错
# 对比 object
def safe_log(value: object) -> None:
print(value)
# print(value.upper()) # Error: object 没有 upper 方法
Any vs object:
| 类型 | 特点 | 使用场景 |
|---|---|---|
Any | 可以进行任何操作 | 动态类型代码、第三方库接口 |
object | 只能使用 object 方法 | 需要类型安全地接受任意值 |
Never 和 NoReturn
Never 和 NoReturn 表示"永远不会返回"的函数:
from typing import Never, NoReturn
def error_exit(message: str) -> Never:
"""永远不会返回,总是抛出异常"""
raise SystemExit(message)
def infinite_loop() -> Never:
"""永远不会返回,无限循环"""
while True:
pass
def assert_never(value: Never) -> Never:
"""用于穷尽性检查"""
raise AssertionError(f"Unexpected value: {value}")
# 用于 match 语句的穷尽性检查
from typing import Literal
Color = Literal["red", "green", "blue"]
def handle_color(color: Color) -> str:
match color:
case "red":
return "红色"
case "green":
return "绿色"
case "blue":
return "蓝色"
case _:
# 如果添加了新颜色但忘记处理,这里会报错
assert_never(color)
Self 类型
Self 表示当前类的实例类型,特别适用于返回 self 的方法或工厂方法:
from typing import Self
class Shape:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
def scale(self, factor: float) -> Self:
"""返回自身的缩放版本"""
return self.__class__(int(self.width * factor), int(self.height * factor))
@classmethod
def square(cls, size: int) -> Self:
"""工厂方法:创建正方形"""
return cls(size, size)
def __enter__(self) -> Self:
return self
class Rectangle(Shape):
pass
# 正确的类型推断
rect: Rectangle = Rectangle(10, 20)
scaled: Rectangle = rect.scale(2) # 类型检查器知道返回 Rectangle
square: Rectangle = Rectangle.square(5) # 也正确
LiteralString
LiteralString 表示只能是字面字符串,用于安全敏感的 API:
from typing import LiteralString
def execute_sql(sql: LiteralString) -> None:
"""执行 SQL 查询,只接受字面字符串防止注入"""
print(f"执行: {sql}")
def dangerous_query(user_input: str) -> None:
# 安全:字面字符串
execute_sql("SELECT * FROM users")
# 安全:字面字符串拼接
table = "users"
execute_sql(f"SELECT * FROM {table}")
# 错误:用户输入不是字面字符串
# execute_sql(f"SELECT * FROM users WHERE name = '{user_input}'")
# 错误:普通字符串变量
# execute_sql(user_input)
Literal 类型
Literal 限制值只能是特定的字面值:
from typing import Literal
Direction = Literal["up", "down", "left", "right"]
Status = Literal["pending", "active", "inactive"]
def move(direction: Direction) -> None:
print(f"移动方向: {direction}")
def set_status(status: Status) -> None:
print(f"状态: {status}")
move("up") # OK
move("down") # OK
# move("forward") # Error: 不是有效的方向
# 数字字面量
Size = Literal[1, 2, 3, 4, 5]
def set_font_size(size: Size) -> None:
print(f"字体大小: {size}")
# 混合类型
Result = Literal["success", "error", 0, 1]
容器类型
列表、集合、字典
# Python 3.9+ 内置泛型(推荐)
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 90, "Bob": 85}
unique_ids: set[int] = {1, 2, 3}
# Python 3.5-3.8 使用 typing 模块
from typing import List, Dict, Set
names: List[str] = ["Alice", "Bob"]
scores: Dict[str, int] = {"Alice": 90}
unique_ids: Set[int] = {1, 2, 3}
元组
元组有特殊的类型注解方式,支持固定长度和可变长度:
# 固定长度元组 - 每个位置的类型都明确
point: tuple[float, float] = (1.0, 2.0)
person: tuple[str, int, str] = ("张三", 25, "北京")
# 可变长度元组 - 所有元素同一类型
numbers: tuple[int, ...] = (1, 2, 3, 4, 5)
# 空元组
empty: tuple[()] = ()
# 解构
def get_point() -> tuple[float, float]:
return (1.0, 2.0)
x, y = get_point() # x: float, y: float
Callable 可调用对象
from collections.abc import Callable
# Callable[[参数类型], 返回类型]
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
def add(a: int, b: int) -> int:
return a + b
result = apply(add, 3, 5) # 8
# 无参数的回调
def on_click(callback: Callable[[], None]) -> None:
callback()
# 任意参数列表
handler: Callable[..., str] = lambda *args: str(args)
# 带默认参数
def process(
data: list[int],
transform: Callable[[int], int] = lambda x: x * 2
) -> list[int]:
return [transform(x) for x in data]
生成器和协程
from collections.abc import Generator, Iterator, AsyncGenerator, AsyncIterator
# 简单生成器
def count_up(n: int) -> Iterator[int]:
for i in range(n):
yield i
# 完整生成器类型 Generator[YieldType, SendType, ReturnType]
def echo_round() -> Generator[int, float, str]:
sent = yield 0
while sent >= 0:
sent = yield round(sent)
return "Done"
# 异步生成器
async def async_count(n: int) -> AsyncIterator[int]:
for i in range(n):
yield i
# 带发送类型的异步生成器
async def async_echo() -> AsyncGenerator[int, str]:
received = yield 0
while True:
received = yield int(received)
泛型(Generics)
泛型允许编写可以处理多种类型的代码,同时保持类型安全。
TypeVar 类型变量
from typing import TypeVar
# 基本类型变量
T = TypeVar('T')
def first_element(lst: list[T]) -> T | None:
"""返回列表的第一个元素"""
return lst[0] if lst else None
# 使用
numbers = [1, 2, 3]
first: int = first_element(numbers) # 推断为 int
strings = ["a", "b", "c"]
first_str: str = first_element(strings) # 推断为 str
# 有界类型变量
Number = TypeVar('Number', int, float)
def add_numbers(a: Number, b: Number) -> Number:
return a + b
add_numbers(1, 2) # OK
add_numbers(1.5, 2.5) # OK
# add_numbers("a", "b") # Error: 不是 int 或 float
# 绑定到基类
class Animal:
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
A = TypeVar('A', bound=Animal)
def make_sound(animal: A) -> str:
return animal.speak()
泛型类
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
def is_empty(self) -> bool:
return len(self._items) == 0
# 使用
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
print(int_stack.pop()) # 2
str_stack: Stack[str] = Stack()
str_stack.push("hello")
print(str_stack.pop()) # hello
# 多类型参数
K = TypeVar('K')
V = TypeVar('V')
class Pair(Generic[K, V]):
def __init__(self, key: K, value: V):
self.key = key
self.value = value
pair: Pair[str, int] = Pair("age", 25)
Python 3.12+ 新语法(PEP 695)
Python 3.12 引入了更简洁的泛型语法,无需导入 TypeVar 和 Generic:
# 泛型函数
def first[T](elements: list[T]) -> T | None:
return elements[0] if elements else None
# 泛型类
class Stack[T]:
def __init__(self):
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# 类型约束
def process[T: (int, float)](value: T) -> T:
return value
# 边界限制
class Container[T: str]:
def __init__(self, value: T):
self.value = value
# 多类型参数
class Map[K, V]:
def __init__(self):
self._data: dict[K, V] = {}
def put(self, key: K, value: V) -> None:
self._data[key] = value
def get(self, key: K) -> V | None:
return self._data.get(key)
# 泛型类型别名
type Vector[T] = list[tuple[T, T]]
type Result[T, E] = tuple[T, None] | tuple[None, E]
Protocol:结构化子类型
Protocol 允许基于结构(方法/属性)而非继承来判断类型兼容性,即"鸭子类型"的静态版本。
基本用法
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None:
...
class Circle:
def draw(self) -> None:
print("绘制圆形")
class Square:
def draw(self) -> None:
print("绘制正方形")
def render(shape: Drawable) -> None:
shape.draw()
# Circle 和 Square 没有继承 Drawable,但都有 draw 方法
render(Circle()) # OK
render(Square()) # OK
class NotDrawable:
pass
# render(NotDrawable()) # Error: 没有 draw 方法
带属性的 Protocol
from typing import Protocol
class Named(Protocol):
name: str
age: int
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
class Animal:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def greet(entity: Named) -> str:
return f"Hello, {entity.name} ({entity.age})"
greet(Person("Alice", 30)) # OK
greet(Animal("Buddy", 5)) # OK
runtime_checkable
使用 @runtime_checkable 装饰器后,可以使用 isinstance() 检查:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Sized(Protocol):
def __len__(self) -> int:
...
class MyList:
def __len__(self) -> int:
return 42
print(isinstance(MyList(), Sized)) # True
print(isinstance([1, 2, 3], Sized)) # True
print(isinstance(42, Sized)) # False
TypedDict
TypedDict 用于精确标注字典的结构,特别是 JSON 数据:
from typing import TypedDict, NotRequired, Required
class Person(TypedDict):
name: str
age: int
email: NotRequired[str] # 可选字段
# 使用
person: Person = {"name": "张三", "age": 25}
person_with_email: Person = {"name": "李四", "age": 30, "email": "[email protected]"}
# 读取
print(person["name"]) # str 类型
# total=False:所有字段都是可选的
class Config(TypedDict, total=False):
host: str
port: int
debug: bool
config: Config = {"host": "localhost"} # OK
# Required 和 NotRequired 混合使用
class PartialConfig(TypedDict, total=False):
host: Required[str] # 必需
port: int # 可选(继承 total=False)
debug: Required[bool] # 必需
ReadOnly 标记(Python 3.13+)
Python 3.13 引入了 typing.ReadOnly(PEP 705),用于标记 TypedDict 中只读的字段:
from typing import TypedDict, ReadOnly
class User(TypedDict):
id: ReadOnly[int] # 只读,创建后不可修改
name: str # 可读可写
email: str # 可读可写
# 创建时可以设置所有字段
user: User = {"id": 1, "name": "张三", "email": "[email protected]"}
# 可以修改可写字段
user["name"] = "李四" # OK
user["email"] = "[email protected]" # OK
# 类型检查器会警告修改只读字段
# user["id"] = 2 # Error: 无法修改只读字段
ReadOnly 与继承
ReadOnly 在继承中的行为:
from typing import TypedDict, ReadOnly, NotRequired
class BaseUser(TypedDict):
id: ReadOnly[int]
name: str
class ExtendedUser(BaseUser):
email: str
role: ReadOnly[str] # 新增只读字段
# ExtendedUser 相当于:
# class ExtendedUser(TypedDict):
# id: ReadOnly[int] # 从父类继承,保持只读
# name: str # 从父类继承,可写
# email: str # 新增,可写
# role: ReadOnly[str] # 新增,只读
ReadOnly 与 NotRequired 组合
from typing import TypedDict, ReadOnly, NotRequired
class APIResponse(TypedDict):
status: ReadOnly[str] # 只读,必需
code: ReadOnly[int] # 只读,必需
message: ReadOnly[NotRequired[str]] # 只读,可选
data: dict[str, object] # 可写,必需
metadata: NotRequired[dict[str, str]] # 可写,可选
# 正确使用
response: APIResponse = {
"status": "success",
"code": 200,
"data": {"result": "ok"}
}
# 可以修改可写字段
response["data"] = {"result": "updated"} # OK
# 不能修改只读字段(类型检查器会警告)
# response["status"] = "error" # Error
实际应用:API 响应
from typing import TypedDict, ReadOnly
class UserResponse(TypedDict):
id: int
username: str
email: str
is_active: bool
class PaginatedResponse(TypedDict):
data: list[UserResponse]
total: ReadOnly[int] # 只读,不应被客户端修改
page: int
per_page: int
def get_users(page: int = 1) -> PaginatedResponse:
# 模拟 API 响应
return {
"data": [
{"id": 1, "username": "alice", "email": "[email protected]", "is_active": True},
{"id": 2, "username": "bob", "email": "[email protected]", "is_active": False}
],
"total": 100,
"page": page,
"per_page": 10
}
response = get_users()
for user in response["data"]:
print(f"{user['username']}: {user['email']}")
不可变数据模型
使用 ReadOnly 创建不可变的配置或数据模型:
from typing import TypedDict, ReadOnly
class ServerConfig(TypedDict):
"""服务器配置,所有字段只读"""
host: ReadOnly[str]
port: ReadOnly[int]
max_connections: ReadOnly[int]
timeout: ReadOnly[float]
def create_server(config: ServerConfig) -> None:
# 配置应该是不可变的,防止意外修改
print(f"启动服务器: {config['host']}:{config['port']}")
# 配置一旦创建就不应修改
config: ServerConfig = {
"host": "localhost",
"port": 8080,
"max_connections": 100,
"timeout": 30.0
}
create_server(config)
# config["port"] = 9090 # 类型检查器会警告
ParamSpec 和 Concatenate
ParamSpec 用于捕获函数的参数签名,Concatenate 用于添加参数:
from typing import ParamSpec, Concatenate, TypeVar, Callable
P = ParamSpec('P')
R = TypeVar('R')
def with_logging(
func: Callable[P, R]
) -> Callable[P, R]:
"""装饰器:添加日志"""
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"调用 {func.__name__}")
result = func(*args, **kwargs)
print(f"返回 {result}")
return result
return wrapper
@with_logging
def add(a: int, b: int) -> int:
return a + b
# Concatenate:添加参数
from collections.abc import Callable
def with_context(
func: Callable[Concatenate[str, P], R]
) -> Callable[P, R]:
"""装饰器:注入上下文参数"""
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
context = "default_context"
return func(context, *args, **kwargs)
return wrapper
@with_context
def process(context: str, data: str) -> str:
return f"[{context}] {data}"
@override 装饰器(Python 3.12+)
@override 明确标记方法是覆盖父类的方法,类型检查器会验证:
from typing import override
class Animal:
def speak(self) -> str:
return "..."
def move(self) -> None:
pass
class Dog(Animal):
@override
def speak(self) -> str:
return "Woof!"
# 错误:父类没有 run 方法
# @override
# def run(self) -> None:
# pass
class Cat(Animal):
@override
def speak(self) -> str:
return "Meow!"
@deprecated 装饰器(Python 3.13+)
Python 3.13 在 warnings 模块中引入了 @deprecated 装饰器(PEP 702),用于在类型系统和运行时标记废弃的函数和类:
基本用法
from warnings import deprecated
from typing import TypeAlias
@deprecated("使用 new_function() 替代")
def old_function(x: int) -> int:
"""这个函数已废弃"""
return x * 2
def new_function(x: int) -> int:
return x * 3
# 调用时会触发 DeprecationWarning
result = old_function(5) # 发出警告
废弃类
@deprecated("使用 NewClass 替代")
class OldClass:
def __init__(self, value: int):
self.value = value
class NewClass:
def __init__(self, value: int):
self.value = value * 2
# 实例化时发出警告
obj = OldClass(10) # DeprecationWarning
废弃方法
class Calculator:
def add(self, a: int, b: int) -> int:
return a + b
@deprecated("使用 add() 方法替代")
def sum(self, a: int, b: int) -> int:
"""已废弃,请使用 add() 方法"""
return self.add(a, b)
calc = Calculator()
calc.sum(1, 2) # DeprecationWarning
类型检查器支持
@deprecated 装饰器与类型检查器集成,在静态分析时就会报告废弃警告:
from warnings import deprecated
@deprecated("将在 2.0 版本中移除")
def legacy_api() -> str:
return "legacy"
# 类型检查器(如 mypy、pyright)会在编辑时显示警告
# 使用废弃函数会在 IDE 中显示删除线
result = legacy_api()
迁移示例
from warnings import deprecated
from typing import overload
# 旧 API 废弃,引导用户迁移到新 API
@deprecated("""
此函数已废弃,请使用 process_data() 替代。
迁移指南:
- old_process(data, True) → process_data(data, mode="strict")
- old_process(data, False) → process_data(data, mode="lenient")
""")
def old_process(data: list[str], strict: bool = True) -> dict[str, int]:
mode = "strict" if strict else "lenient"
return process_data(data, mode=mode)
def process_data(data: list[str], mode: str = "strict") -> dict[str, int]:
"""新的数据处理函数"""
if mode == "strict":
return {s: len(s) for s in data if s}
return {s: len(s) for s in data}
与 typing 组合使用
from warnings import deprecated
from typing import TypeAlias, Callable
# 废弃类型别名
@deprecated("使用 UserID 替代")
type OldUserID = str
type UserID = int
# 废弃回调类型
@deprecated("使用 modern_callback 类型替代")
OldCallback: TypeAlias = Callable[[str], None]
type ModernCallback = Callable[[str, dict[str, object]], None]
最佳实践
from warnings import deprecated
from typing import overload
class APIClient:
@overload
@deprecated("使用 get(url, params=None) 替代")
def fetch(self, url: str) -> dict: ...
@overload
def get(self, url: str, params: dict[str, str] | None = None) -> dict: ...
def fetch(self, url: str) -> dict:
"""废弃的方法,重定向到新方法"""
return self.get(url)
def get(self, url: str, params: dict[str, str] | None = None) -> dict:
"""推荐使用的新方法"""
# 实际实现
return {"url": url, "params": params}
注意事项:
@deprecated装饰器应放在最外层(在@overload之后)- 废弃消息应清晰说明替代方案和迁移路径
- 可以在消息中包含版本信息和截止日期
类型收窄(Type Narrowing)
类型检查器可以通过条件判断自动收窄类型:
from typing import Union
def process(value: Union[int, str]) -> str:
# 类型检查器根据 isinstance 自动收窄
if isinstance(value, int):
# 这里 value 是 int 类型
return str(value * 2)
else:
# 这里 value 是 str 类型
return value.upper()
# None 检查
def handle_optional(value: str | None) -> str:
if value is None:
return "empty"
# 这里 value 确定是 str
return value.upper()
TypeGuard
TypeGuard 用于自定义类型守卫函数:
from typing import TypeGuard, Any
def is_string_list(value: list[Any]) -> TypeGuard[list[str]]:
"""检查列表是否全是字符串"""
return all(isinstance(x, str) for x in value)
def process(items: list[Any]) -> None:
if is_string_list(items):
# 这里 items 被收窄为 list[str]
print(items[0].upper()) # OK
TypeIs(Python 3.13+)
TypeIs 是 Python 3.13 引入的新特性,比 TypeGuard 提供更直观的类型收窄行为。
TypeIs vs TypeGuard
TypeGuard 和 TypeIs 的关键区别在于类型收窄的方向:
- TypeGuard:当守卫函数返回
True时,类型被收窄为指定的类型 - TypeIs:更符合直觉,返回
True时类型被收窄,返回False时类型被排除
from typing import TypeGuard, TypeIs
# TypeGuard 示例
def is_str_typeguard(value: str | int) -> TypeGuard[str]:
return isinstance(value, str)
def process_typeguard(value: str | int) -> None:
if is_str_typeguard(value):
# value 被收窄为 str
print(value.upper())
else:
# value 仍然是 str | int(TypeGuard 的限制)
# 类型检查器不知道这里 value 一定是 int
pass
# TypeIs 示例(Python 3.13+)
def is_str_typeis(value: str | int) -> TypeIs[str]:
return isinstance(value, str)
def process_typeis(value: str | int) -> None:
if is_str_typeis(value):
# value 被收窄为 str
print(value.upper())
else:
# value 被收窄为 int(TypeIs 的优势)
print(value * 2) # 安全:value 确定是 int
实际应用示例
from typing import TypeIs
def is_non_empty_string(value: str | None) -> TypeIs[str]:
"""检查值是否为非空字符串"""
return value is not None and len(value) > 0
def process_input(value: str | None) -> str:
if is_non_empty_string(value):
# value 确定是非空字符串
return value.upper()
else:
# value 是 None 或空字符串
return "默认值"
def is_positive(n: int) -> TypeIs[int]:
"""检查整数是否为正数"""
return n > 0
def process_number(n: int) -> str:
if is_positive(n):
# n 确定是正整数
return f"正数: {n}"
else:
# n 确定是非正整数(0 或负数)
return f"非正数: {n}"
# 复杂类型检查
def is_user_dict(value: dict[str, object]) -> TypeIs[dict[str, str]]:
"""检查字典的所有值是否都是字符串"""
return all(isinstance(v, str) for v in value.values())
def process_data(data: dict[str, object]) -> None:
if is_user_dict(data):
# data 确定是 dict[str, str]
for key, value in data.items():
print(f"{key}: {value.upper()}") # 安全调用字符串方法
TypeIs 的设计原理
TypeIs 的名称和语义来自类型理论中的"类型即命题"对应关系:
- 类型
T对应命题"值x的类型是T" TypeIs[T]意味着"返回值断言参数的类型是否为T"
这种设计让类型收窄更加符合直觉,特别是在 else 分支中。
类型注解最佳实践
1. 逐步添加类型
# 从无类型开始
def process(data):
return data.get("value", 0)
# 逐步添加参数类型
def process(data: dict) -> int:
return data.get("value", 0)
# 最终完善
def process(data: dict[str, int]) -> int:
return data.get("value", 0)
2. 使用类型别名简化复杂类型
# 不好:复杂的嵌套类型
def process(
data: dict[str, list[tuple[str, int, dict[str, float]]]]
) -> None:
pass
# 好的:使用类型别名
type Record = tuple[str, int, dict[str, float]]
type DataStore = dict[str, list[Record]]
def process(data: DataStore) -> None:
pass
3. 避免过度使用 Any
# 不好
def process(data: Any) -> Any:
return data
# 好的:明确类型
def process(data: dict[str, int]) -> dict[str, int]:
return data
# 如果确实需要任意类型,使用泛型
def process[T](data: T) -> T:
return data
4. 使用 mypy 进行类型检查
# 安装
pip install mypy
# 运行类型检查
mypy your_file.py
# 严格模式
mypy --strict your_file.py
5. 为第三方库添加类型存根
# third_party.pyi
def untyped_function(arg: int) -> str: ...
typing 模块常用类型速查表
| 类型 | 说明 | 示例 |
|---|---|---|
list[T] | 列表 | list[int] |
dict[K, V] | 字典 | dict[str, int] |
set[T] | 集合 | set[str] |
tuple[T, ...] | 可变元组 | tuple[int, ...] |
tuple[T1, T2] | 固定元组 | tuple[str, int] |
Union[A, B] / A | B | 联合类型 | int | str |
Optional[T] / T | None | 可选类型 | str | None |
Any | 任意类型 | Any |
NoReturn / Never | 永不返回 | def exit() -> Never |
Literal[...] | 字面量 | Literal["a", "b"] |
Callable[[Args], R] | 可调用对象 | Callable[[int], str] |
Type[T] / type[T] | 类型对象 | type[int] |
Protocol | 协议 | class Drawable(Protocol) |
TypedDict | 类型字典 | class Person(TypedDict) |
TypeVar | 类型变量 | T = TypeVar('T') |
Generic[T] | 泛型基类 | class Stack(Generic[T]) |
NewType | 新类型 | UserId = NewType('UserId', int) |
Self | 自身类型 | def method(self) -> Self |
LiteralString | 字面字符串 | def sql(query: LiteralString) |
ParamSpec | 参数规格 | P = ParamSpec('P') |
Concatenate | 参数连接 | Callable[Concatenate[int, P], R] |
TypeGuard | 类型守卫 | def is_str(x) -> TypeGuard[str] |
TypeIs | 类型判断(3.13+) | def is_str(x) -> TypeIs[str] |
ReadOnly | 只读字段(3.13+) | class D(TypedDict): x: ReadOnly[int] |
Final | 最终值 | PI: Final = 3.14 |
@override | 覆盖标记 | @override def method(self) |
@deprecated | 废弃标记(3.13+) | @deprecated("使用 new_func 替代") |
小结
本章系统学习了 Python 类型注解:
- 基本类型注解:变量、函数参数、返回值的类型标注
- 联合类型:
Union、Optional和|语法 - 特殊类型:
Any、Never、Self、LiteralString、Literal - 容器类型:列表、字典、集合、元组、Callable、生成器
- 泛型:
TypeVar、Generic和 Python 3.12+ 新语法 - Protocol:结构化子类型,静态鸭子类型
- TypedDict:精确标注字典结构,Python 3.13+ 支持
ReadOnly - 类型收窄:
TypeGuard、TypeIs(Python 3.13+) - 废弃标记:
@deprecated装饰器(Python 3.13+) - 最佳实践:逐步添加、使用别名、避免 Any
Python 版本特性对照
| 特性 | Python 版本 |
|---|---|
| 基本类型注解 | 3.5+ |
Union、Optional | 3.5+ |
Literal | 3.8+ |
TypedDict | 3.8+ |
| ` | ` 联合类型语法 |
ParamSpec、Concatenate | 3.10+ |
Self、LiteralString | 3.11+ |
@override | 3.12+ |
type 语句、类型参数语法 | 3.12+ |
TypeIs、ReadOnly、@deprecated | 3.13+ |
练习
- 为一个学生信息管理系统添加完整的类型注解
- 使用 Protocol 定义一个可序列化协议
- 使用 TypedDict 定义 REST API 响应类型,包含 ReadOnly 字段
- 创建一个泛型缓存类,支持类型安全的存取
- 使用
@override实现一个继承层次结构 - 使用
@deprecated标记一个废弃的函数,并提供迁移指南 - 使用
TypeIs实现一个类型守卫函数,在 else 分支正确收窄类型