正在加载,请稍候…

Python 类型提示:实用指南与示例

从基础到高级模式学习 Python 类型提示,涵盖 Union、Optional、TypedDict、Protocol、泛型以及如何使用 mypy 进行静态类型

Python 类型提示:实用指南与示例

为什么 Python 类型提示很重要

Python 是动态类型语言,但类型提示(自 Python 3.5 引入,3.12+ 有重大改进)允许你用期望的类型注释代码。它们不影响运行时行为——Python 在执行时仍然忽略它们——但它们支持:

  • 静态类型检查,使用 mypy、Pyright 或类似工具
  • 更好的 IDE 支持——自动补全、重构和内联文档
  • 自文档化 API——函数签名传达意图,无需文档字符串
  • 早期发现错误——在代码运行之前

Python 类型提示:实用指南与示例 插图

基本注解

# 变量
name: str = "Alice"
age: int = 30
score: float = 9.8
active: bool = True

# 函数参数和返回类型
def greet(name: str) -> str:
    return f"Hello, {name}!"

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

def process(items: list) -> None:  # 无返回值
    for item in items:
        print(item)

内置集合类型(Python 3.9+)

# Python 3.9+ — 直接使用内置类型
def get_ids() -> list[int]:
    return [1, 2, 3]

def get_config() -> dict[str, str]:
    return {"host": "localhost", "port": "5432"}

def get_scores() -> tuple[int, int, int]:
    return (95, 87, 92)

def get_unique_tags() -> set[str]:
    return {"python", "typing", "tutorial"}

# Python 3.8 及以下 — 使用 typing 模块
from typing import List, Dict, Tuple, Set

def get_ids() -> List[int]: ...
def get_config() -> Dict[str, str]: ...

Optional 和 Union 类型

from typing import Optional, Union  # 或 Python 3.10+ 使用 |

# Optional[T] 等价于 Union[T, None]
def find_user(user_id: int) -> Optional[str]:  # str 或 None
    if user_id > 0:
        return "Alice"
    return None

# Python 3.10+ 语法
def find_user(user_id: int) -> str | None:  # 等价
    ...

# Union:可以是多种类型
def process(value: Union[int, str]) -> str:
    return str(value)

# Python 3.10+ 联合语法
def process(value: int | str) -> str:
    return str(value)

# 常见错误——对可空类型永远不要这样做
def bad(name: str = None) -> None:  # 类型检查器会报错
    pass

def good(name: str | None = None) -> None:  # 正确
    pass

TypedDict——类型化字典

from typing import TypedDict

class UserDict(TypedDict):
    id: int
    name: str
    email: str

class PartialUserDict(TypedDict, total=False):  # 所有键可选
    name: str
    email: str

def create_user(data: UserDict) -> UserDict:
    return data

# 类型安全:mypy 检查键名和值类型
user: UserDict = {
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
}

create_user({"id": 1, "name": "Alice"})  # ❌ mypy 错误:缺少 email
create_user({"id": "1", "name": "Alice", "email": "a@b.com"})  # ❌ id 必须是 int

Python 类型提示:实用指南与示例 插图

泛型

from typing import TypeVar, Generic

T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')

# 泛型函数
def first(items: list[T]) -> T | None:
    return items[0] if items else None

result = first([1, 2, 3])    # 推断类型:int | None
result = first(["a", "b"])   # 推断类型:str | None

# 泛型类
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 | None:
        return self._items[-1] if self._items else None

stack: Stack[int] = Stack()
stack.push(1)
stack.push("hello")  # ❌ mypy 错误:期望 int,得到 str

Protocol——结构类型(鸭子类型)

from typing import Protocol

# 定义类型必须能做什么
class Drawable(Protocol):
    def draw(self) -> None: ...

class Serializable(Protocol):
    def to_json(self) -> str: ...

# 任何具有 draw() 方法的类都满足 Drawable——无需继承
class Circle:
    def draw(self) -> None:
        print("Drawing circle")

class Square:
    def draw(self) -> None:
        print("Drawing square")

def render_all(shapes: list[Drawable]) -> None:
    for shape in shapes:
        shape.draw()

# ✅ 有效,因为 Circle 和 Square 都有 draw()
render_all([Circle(), Square()])

# 对于无法修改的第三方类型很有用
class FileWriter(Protocol):
    def write(self, data: bytes) -> int: ...
    def close(self) -> None: ...

def save_data(writer: FileWriter, data: bytes) -> None:
    writer.write(data)
    writer.close()

Callable 类型

from typing import Callable

# 一个接受 int 并返回 str 的函数
def apply(func: Callable[[int], str], value: int) -> str:
    return func(value)

# 可变参数
def run(callback: Callable[..., None]) -> None:
    callback()

# 返回可调用对象(高阶函数)
def make_multiplier(factor: int) -> Callable[[int], int]:
    def multiply(x: int) -> int:
        return x * factor
    return multiply

Literal 类型

from typing import Literal

Direction = Literal['north', 'south', 'east', 'west']
HttpMethod = Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH']

def move(direction: Direction) -> None:
    print(f"Moving {direction}")

move('north')    # ✅
move('diagonal') # ❌ mypy 错误:不是有效的 Direction

def make_request(method: HttpMethod, url: str) -> None: ...

Python 类型提示:实用指南与示例 插图

dataclasses 和 attrs 与类型

from dataclasses import dataclass, field

@dataclass
class Product:
    id: int
    name: str
    price: float
    tags: list[str] = field(default_factory=list)
    active: bool = True

    def discount_price(self, pct: float) -> float:
        return self.price * (1 - pct)

p = Product(id=1, name="Widget", price=9.99)
p.discount_price(0.1)  # 8.991

类型收窄

def process(value: int | str) -> str:
    if isinstance(value, int):
        # mypy 现在知道在此分支中 value: int
        return str(value * 2)
    else:
        # mypy 现在知道在此分支中 value: str
        return value.upper()

# assert 收窄类型
def get_name(user: dict | None) -> str:
    assert user is not None, "user must not be None"
    # mypy 知道此处 user: dict
    return user.get("name", "")

# TypeGuard 用于自定义类型收窄函数
from typing import TypeGuard

def is_string_list(lst: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in lst)

def join_strings(items: list[object]) -> str:
    if is_string_list(items):
        return ", ".join(items)  # mypy 知道 items: list[str]
    return ""

运行 mypy

# 安装
pip install mypy

# 检查文件
mypy script.py

# 使用严格模式(推荐用于新项目)
mypy --strict script.py

# 常用标志
mypy --ignore-missing-imports  # 跳过无类型注解的第三方库
mypy --check-untyped-defs      # 类型检查无注解的函数

# pyproject.toml 配置
[tool.mypy]
strict = true
ignore_missing_imports = true

常见问题

问:类型提示会减慢 Python 吗? 不会。Python 在运行时忽略注解(它们作为元数据存储,默认不评估)。使用 from __future__ import annotations 导入使它们成为字符串,开销更小。

问:我应该注解每个变量吗? 只在类型从上下文不明显时注解。函数签名应始终注解。变量赋值中类型从值明确(例如 x = 5)不需要注解。

问:如何处理没有类型存根的第三方库? 在特定行使用 # type: ignore,使用 py.typed 存根,或安装存根包(pip install types-requests)。查看 typeshed 获取社区维护的存根。

→ 使用 JSON Viewer 探索和验证 Python 数据结构。