上一篇【第 014 篇】装饰器——Python 魔法的精髓
下一篇【第 016 篇】并发编程(一)——多线程与多进程


系列说明:本系列共 30 篇,全面介绍 Python 编程从零基础到软件工程师的完整路径。本文为第 015 篇,深入讲解 Python 类型注解系统与 mypy 静态类型检查——理解并正确使用类型注解能显著提升代码质量,减少运行时错误。


摘要

Python 是一门动态类型语言,变量类型在运行时才确定。这种灵活性虽然提升了开发效率,但也增加了运行时类型错误的风险。Python 3.5 引入的类型注解(Type Hints)允许开发者显式声明变量、函数参数和返回值的类型,配合 mypy 等静态类型检查工具,可以在不运行程序的情况下发现类型错误。本文涵盖类型注解基础语法、内置类型与泛型、TypeAlias 与类型别名、自定义类型类、Protocol 结构化子类型,以及 mypy 的实战使用技巧。


一、为什么需要类型注解

1.1 动态类型的陷阱

# 运行时才发现类型错误
def calculate_total(prices):
    return sum(prices)

# 测试通过
print(calculate_total([10, 20, 30]))  # 60

# 生产环境出错(传入字典而非列表)
try:
    print(calculate_total({"a": 10, "b": 20}))
except TypeError as e:
    print(f"错误:{e}")
# TypeError: unsupported operand type(s) for +: 'int' and 'str'

这种错误在测试不充分时可能被忽视,直到生产环境才暴露。

1.2 类型注解的价值

# 声明预期类型
from typing import List

def calculate_total(prices: List[int]) -> int:
    return sum(prices)

# IDE/编辑器可以提供:
# - 自动补全增强
# - 类型错误即时提示
# - 重构时的安全保障

print(calculate_total([10, 20, 30]))  # 60
print(calculate_total([10, 20]))       # 30

1.3 类型注解的限制

类型注解是可选的,Python 解释器会忽略它们:

def greet(name: str) -> str:
    return f"Hello, {name}"

greet(123)  # 完全合法,Python 不检查类型
# mypy 或 IDE 会报告类型错误

类型注解的主要价值在于:静态检查、IDE 辅助、代码文档化。


二、基础类型注解

2.1 变量注解

# Python 3.6+
name: str = "Alice"
age: int = 30
height: float = 1.75
is_active: bool = True
scores: list = [90, 85, 88]  # 原始类型

# Python 3.9+ 内置集合类型
names: list = ["Alice", "Bob"]
scores_dict: dict = {"math": 90, "english": 85}
scores_set: set = {90, 85, 88}

# Python 3.9+ 推荐写法(使用内置类型)
from collections.abc import Sequence, Mapping

def process_items(items: Sequence[int]) -> None:
    for item in items:
        print(item)

2.2 函数注解

def add(a: int, b: int) -> int:
    return a + b

def greet(name: str, prefix: str = "Hello") -> str:
    return f"{prefix}, {name}!"

# 多返回值
from typing import Tuple

def divide(a: float, b: float) -> Tuple[float, float]:
    quotient = a // b
    remainder = a % b
    return quotient, remainder

# 无返回值
def print_sum(a: int, b: int) -> None:
    print(f"{a} + {b} = {a + b}")

2.3 类属性注解

class User:
    name: str
    email: str
    age: int
    
    def __init__(self, name: str, email: str, age: int) -> None:
        self.name = name
        self.email = email
        self.age = age
    
    def introduce(self) -> str:
        return f"我是 {self.name}{self.age} 岁"

# Python 3.10+ 可以直接在 __init__ 中注解实例属性
class Product:
    def __init__(
        self,
        name: str,
        price: float,
        quantity: int = 0
    ) -> None:
        self.name = name
        self.price = price
        self.quantity = quantity
    
    def total_value(self) -> float:
        return self.price * self.quantity

三、typing 模块详解

3.1 泛型容器类型

from typing import List, Dict, Set, Tuple, FrozenSet

# 列表
names: List[str] = ["Alice", "Bob"]

# 字典
scores: Dict[str, int] = {"Alice": 90, "Bob": 85}

# 集合
unique_ids: Set[int] = {1, 2, 3, 4, 5}

# 元组(固定长度和类型)
point: Tuple[float, float] = (1.5, 2.5)
rgb_color: Tuple[int, int, int] = (255, 128, 0)

# 冻结集合(不可变)
prime_numbers: FrozenSet[int] = frozenset([2, 3, 5, 7])

# 嵌套类型
matrix: List[List[int]] = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Python 3.9+ 可以使用内置类型
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 90}

3.2 Union 和 Optional

from typing import Union, Optional

# Union:多种可能类型之一
def process_value(value: Union[int, float, str]) -> str:
    return str(value)

# Optional:可以是 None 或某种类型(等价于 Union[T, None])
def find_user(user_id: int) -> Optional[str]:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)  # 如果不存在返回 None

# 链式 Union
Number = Union[int, float, complex]

def calculate(n: Number) -> float:
    return float(abs(n))

# Python 3.10+ 可以使用 | 操作符
def process(value: int | float | str) -> str:
    return str(value)

def find_user(user_id: int) -> str | None:
    return {"Alice": "Alice"}.get(user_id)

3.3 Any 和 Callable

from typing import Any, Callable, TypeVar

# Any:任意类型(禁用类型检查)
def log(message: Any) -> None:
    print(f"[LOG] {message}")

log("hello")      # 正确
log(123)          # 正确
log({"key": 1})   # 正确

# 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

def multiply(a: int, b: int) -> int:
    return a * b

print(apply(add, 3, 4))      # 7
print(apply(multiply, 3, 4)) # 12

# 无返回值的 Callable
def on_click(handler: Callable[[], None]) -> None:
    handler()

# 带任意参数的 Callable
def execute(callback: Callable[..., Any]) -> Any:
    return callback()

# 类型变量
T = TypeVar("T")
U = TypeVar("U")

def first_element(items: list[T]) -> Optional[T]:
    return items[0] if items else None

def pair(a: T, b: U) -> Tuple[T, U]:
    return (a, b)

print(first_element([1, 2, 3]))       # 2
print(first_element(["a", "b"]))      # b
print(pair(1, "one"))                  # (1, 'one')

3.4 类型别名

from typing import List, Dict, Tuple, TypeAlias

# 简单类型别名
UserId = int
UserName = str

# 复杂类型别名
Matrix = List[List[float]]
Connection = Tuple[str, int]
ConfigDict = Dict[str, Union[str, int, bool]]

# 类型别名
Coordinates: TypeAlias = Tuple[float, float]
RGBColor: TypeAlias = Tuple[int, int, int]
Result: TypeAlias = Union[dict, list, str, None]

def point_in_circle(
    center: Coordinates,
    radius: float,
    point: Coordinates
) -> bool:
    import math
    dx = point[0] - center[0]
    dy = point[1] - center[1]
    return math.sqrt(dx**2 + dy**2) <= radius

center: Coordinates = (0.0, 0.0)
point: Coordinates = (1.0, 1.0)
print(point_in_circle(center, 2.0, point))  # True

# Python 3.10+ 简化写法
type Matrix = list[list[float]]
type RGBColor = tuple[int, int, int]

3.5 Literal 和 Never

from typing import Literal, Never

# Literal:精确的字面值
def move(direction: Literal["up", "down", "left", "right"]) -> Tuple[int, int]:
    movements = {
        "up": (0, 1),
        "down": (0, -1),
        "left": (-1, 0),
        "right": (1, 0)
    }
    return movements[direction]

print(move("up"))      # 正确
print(move("left"))    # 正确
# print(move("diagonal"))  # 类型错误

# Never:永不返回的类型
def unreachable() -> Never:
    raise RuntimeError("不应该到达这里")

# 或者用于标记不可能的分支
def process_value(value: int | str) -> str:
    match value:
        case int():
            return f"整数: {value}"
        case str():
            return f"字符串: {value}"
        case _:
            # 在 exhaustive match 下,这个分支永远不会执行
            print("Unexpected type")
            exit(1)

四、自定义类型和 Protocol

4.1 NewType 创建类型标识

from typing import NewType, List

# 创建语义明确的类型
UserId = NewType("UserId", int)
ProductId = NewType("ProductId", int)
OrderId = NewType("OrderId", int)

def get_user(user_id: UserId) -> dict:
    return {"id": user_id, "name": "Alice"}

def get_product(product_id: ProductId) -> dict:
    return {"id": product_id, "name": "Widget"}

# 类型安全:不同 ID 不能混用
user_id = UserId(123)
product_id = ProductId(456)

print(get_user(user_id))     # 正确
# print(get_user(product_id))  # 类型错误!

# NewType 仍然基于底层类型
print(UserId(123) + 1)  # 124

4.2 Protocol 结构化子类型

from typing import Protocol, runtime_checkable

# 定义协议(类似接口)
@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...
    def get_area(self) -> float: ...

class Circle:
    def __init__(self, radius: float) -> None:
        self.radius = radius
    
    def draw(self) -> None:
        print(f"绘制圆,半径={self.radius}")
    
    def get_area(self) -> float:
        import math
        return math.pi * self.radius ** 2

class Square:
    def __init__(self, side: float) -> None:
        self.side = side
    
    def draw(self) -> None:
        print(f"绘制正方形,边长={self.side}")
    
    def get_area(self) -> float:
        return self.side ** 2

# 函数接受任何实现了 Drawable 协议的类型
def render_all(shapes: list[Drawable]) -> None:
    for shape in shapes:
        shape.draw()
        print(f"  面积: {shape.get_area():.2f}")

circle = Circle(5)
square = Square(4)
render_all([circle, square])  # 都可以正常工作

4.3 泛型类

from typing import Generic, TypeVar

T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")

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:
        if not self._items:
            raise IndexError("Pop from empty stack")
        return self._items.pop()
    
    def peek(self) -> T:
        if not self._items:
            raise IndexError("Peek from empty stack")
        return self._items[-1]
    
    def is_empty(self) -> bool:
        return len(self._items) == 0

class DictMap(Generic[K, V]):
    """泛型字典包装器。"""
    
    def __init__(self) -> None:
        self._data: dict[K, V] = {}
    
    def set(self, key: K, value: V) -> None:
        self._data[key] = value
    
    def get(self, key: K) -> V | None:
        return self._data.get(key)
    
    def items(self) -> list[tuple[K, V]]:
        return list(self._data.items())

# 使用泛型类
int_stack = Stack[int]()
int_stack.push(1)
int_stack.push(2)
print(int_stack.pop())  # 2

str_stack = Stack[str]()
str_stack.push("hello")
print(str_stack.pop())  # hello

str_int_map = DictMap[str, int]()
str_int_map.set("score", 100)
print(str_int_map.get("score"))  # 100

五、mypy 静态类型检查

5.1 安装和基本使用

# 安装 mypy
pip install mypy

# 在项目目录运行
mypy .

# 检查特定文件
mypy src/main.py

# 检查整个项目并递归
mypy src/

5.2 mypy 配置

创建 mypy.ini 或 pyproject.toml 配置:

# mypy.ini
[mypy]
python_version = 3.11
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
strict_equality = True

[mypy-pytest.*]
ignore_missing_imports = True

[mypy-django.*]
ignore_missing_imports = True
# pyproject.toml (推荐)
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
strict = true

[[tool.mypy.overrides]]
module = ["pytest.*", "django.*"]
ignore_missing_imports = true

5.3 常见 mypy 错误

# 错误1:类型不匹配
def add(a: int, b: int) -> int:
    return a + b

add("hello", "world")  # mypy: Argument 1 to "add" has incompatible type "str"; expected "int"

# 错误2:返回类型不匹配
def get_name() -> str:
    return 123  # mypy: Return type "int" of "get_name" does not match return type "str"

# 错误3:可能返回 None
def find_user(user_id: int) -> str:
    users = {1: "Alice"}
    return users[user_id]  # 如果 key 不存在会返回 None

# 正确写法
def find_user(user_id: int) -> str | None:
    users = {1: "Alice"}
    return users.get(user_id)

# 错误4:缺少类型注解
def process(data):  # mypy: Function is missing a type annotation
    return data

# 正确写法
def process(data: list[int]) -> int:
    return sum(data)

5.4 忽略类型检查

from typing import Any

# 类型: ignore:忽略特定行的类型检查
result = some_function()  # type: ignore

# type: ignore[code]:忽略特定错误代码
value: int = "string"  # type: ignore[assignment]

# mypy: ignore:忽略整个文件
# 在文件顶部添加:# mypy: ignore-errors

六、实战演练

6.1 类型化数据类

from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime

@dataclass
class Address:
    street: str
    city: str
    country: str
    postal_code: str

@dataclass
class Employee:
    id: int
    name: str
    email: str
    department: str
    salary: float
    hire_date: datetime
    address: Optional[Address] = None
    manager_id: Optional[int] = None

@dataclass
class Department:
    name: str
    budget: float
    employees: List[Employee] = None  # 默认值为 None,类型是 Optional[List[Employee]]
    
    def __post_init__(self) -> None:
        if self.employees is None:
            self.employees = []
    
    def total_salary(self) -> float:
        return sum(emp.salary for emp in self.employees)
    
    def find_employee(self, emp_id: int) -> Optional[Employee]:
        for emp in self.employees:
            if emp.id == emp_id:
                return emp
        return None

# 使用
dept = Department(
    name="Engineering",
    budget=1_000_000.0
)
dept.employees.append(Employee(
    id=1,
    name="Alice",
    email="alice@company.com",
    department="Engineering",
    salary=80000.0,
    hire_date=datetime.now()
))

print(f"部门总工资:{dept.total_salary()}")  # 80000.0

6.2 类型化函数式工具

from typing import TypeVar, Callable, List, Optional, Iterator

T = TypeVar("T")
U = TypeVar("U")
R = TypeVar("R")

def map_(func: Callable[[T], U], iterable: List[T]) -> List[U]:
    """类型安全的 map。"""
    return [func(item) for item in iterable]

def filter_(pred: Callable[[T], bool], iterable: List[T]) -> List[T]:
    """类型安全的 filter。"""
    return [item for item in iterable if pred(item)]

def reduce_(
    func: Callable[[T, T], T],
    iterable: List[T],
    initial: T
) -> T:
    """类型安全的 reduce。"""
    result = initial
    for item in iterable:
        result = func(result, item)
    return result

def pipeline(*functions: Callable[[T], T]) -> Callable[[T], T]:
    """函数组合器。"""
    def compose(value: T) -> T:
        result = value
        for func in functions:
            result = func(result)
        return result
    return compose

# 使用
numbers = [1, 2, 3, 4, 5]
doubled = map_(lambda x: x * 2, numbers)
evens = filter_(lambda x: x % 2 == 0, numbers)
total = reduce_(lambda a, b: a + b, numbers, 0)

print(f"加倍:{doubled}")      # [2, 4, 6, 8, 10]
print(f"偶数:{evens}")        # [2, 4]
print(f"总和:{total}")        # 15

# 管道组合
process = pipeline(
    lambda x: x * 2,
    lambda x: x + 1,
    lambda x: x ** 2
)
print(f"管道结果:{process(3)}")  # 64 ((3 * 2 + 1) ** 2)

6.3 类型化 API 响应

from typing import List, Optional, Generic, TypeVar
from dataclasses import dataclass
import json

T = TypeVar("T")

@dataclass
class ApiResponse(Generic[T]):
    success: bool
    data: Optional[T] = None
    error: Optional[str] = None
    total: Optional[int] = None

@dataclass
class User:
    id: int
    name: str
    email: str
    is_active: bool

@dataclass
class PaginatedUsers:
    users: List[User]
    page: int
    page_size: int
    total: int

def parse_response(
    json_str: str,
    model_class: type[T]
) -> ApiResponse[T]:
    """解析 JSON 响应。"""
    try:
        data = json.loads(json_str)
        return ApiResponse(
            success=True,
            data=model_class(**data)
        )
    except (json.JSONDecodeError, TypeError) as e:
        return ApiResponse(
            success=False,
            error=str(e)
        )

# 模拟 API 响应
response_json = json.dumps({
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
    "is_active": True
})

response = parse_response(response_json, User)
if response.success:
    print(f"用户:{response.data.name}")  # Alice
else:
    print(f"错误:{response.error}")

七、常见问题与注意事项

7.1 循环导入问题

# models.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .views import UserView

class User:
    name: str
    
    # 在 TYPE_CHECKING 块中导入避免循环
    def render(self, view: "UserView") -> str:
        return view.display(self)

7.2 类型注解与默认值顺序

from typing import Optional

# 正确:类型注解在前,默认值在后
def create_user(
    name: str,
    email: Optional[str] = None,
    age: int = 0
) -> None:
    pass

7.3 动态类型与反射

# setattr/getattr 与类型注解
class User:
    name: str

user = User()
# 动态设置属性
setattr(user, "age", 30)  # 类型检查器无法追踪

# 解决方案:使用 __slots__ 或类型化字典
from dataclasses import dataclass, field

@dataclass
class TypedUser:
    name: str
    attributes: dict[str, int] = field(default_factory=dict)

user = TypedUser(name="Alice")
user.attributes["age"] = 30  # 类型安全的方式

7.4 Python 版本兼容

# Python 3.9 之前的写法
from typing import List, Dict, Optional

def process(items: List[int]) -> Optional[int]:
    return items[0] if items else None

# Python 3.9+ 的写法(更简洁)
def process(items: list[int]) -> int | None:
    return items[0] if items else None

# 建议:保持向后兼容
from __future__ import annotations  # Python 3.7+ 允许延迟评估注解

八、总结

类型注解是 Python 3.5+ 引入的强大功能,它让我们可以显式声明变量和函数的类型,配合 mypy 等静态检查工具,在不运行程序的情况下发现潜在的类型错误。

本文的核心要点:类型注解是可选的,Python 解释器会忽略它们,但 IDE 和静态检查工具会利用这些信息。typing 模块提供了丰富的类型工具,包括 Union、Optional、Callable、TypeVar 和 Protocol。Protocol 定义结构化子类型,实现类似接口的功能。mypy 是最流行的 Python 静态类型检查器,可以集成到 CI/CD 流程中。类型注解不仅提升代码质量,还是最好的文档形式之一。


上一篇【第 014 篇】装饰器——Python 魔法的精髓
下一篇【第 016 篇】并发编程(一)——多线程与多进程


参考资料

  1. Python 官方文档 - typing
  2. mypy 官方文档
  3. PEP 484 - Type Hints
  4. Python Type Checking Guide - Real Python
  5. mypy cheat sheet
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐