跳转至

第 8 章 · 类型提示

作为资深 Java 工程师,你最怀念的可能是编译期的类型安全网。Python 是动态类型,但通过类型提示(type hints) + 静态检查工具(mypy/pyright),你能拿回接近 Java 的类型安全——而且是渐进式的:想加多少加多少,从关键模块开始。


8.1 类型提示:可选、不强制、给工具看

类型注解写在变量、参数、返回值上,运行时不强制(只是元数据):

def greet(name: str, times: int = 1) -> str:
    return f"Hi {name}! " * times

age: int = 30
# 这能跑,运行时不报错(注解不强制)
greet("Alice", "not a number")   # ⚠️ 但 mypy 会警告你

真正干活的是静态检查器mypypyright。它们在"运行前"分析代码,像 Java 编译器那样抓类型错误。

pip install mypy
mypy your_module.py

8.2 容器类型注解(泛型)

Python 3.9+ 直接用小写容器名做泛型(不用再 from typing import List):

  • Java

    List<Integer> nums;
    Map<String, User> users;
    
  • Python(3.9+)

    nums: list[int]
    users: dict[str, User]
    unique: set[str]
    pair: tuple[int, str]          # 固定长度:一个 int 一个 str
    many: tuple[int, ...]          # 任意长度的 int 元组
    

现代写法

旧代码里你会看到 from typing import List, Dict,那是 3.8 及以前的写法。3.9+ 直接 list[int]dict[str, int],更简洁。本教程基线 3.13,一律用新写法。


8.3 Optional / 联合类型 X | Y

  • Java

    Integer age;          // null 表示可能缺失
    String findName();    // 可能返回 null
    
  • Python(3.10+)

    age: int | None              # 可能是 int 或 None
    def find_name() -> str | None: ...
    

X | None 是现代写法(等价于旧式 Optional[X])。X | Y | Z 表示"联合类型",比 Java 的类型系统更灵活——一个值可以合法地属于多种类型:

def parse(value: int | str) -> int:
    return int(value)

⚠️ Java 程序员的陷阱

Java 里 Integer 默认可空、int 不可空,混在一起很乱。Python 的 int 本身不含"可能为 None"的语义——要表达可空,必须显式写 int | None。这让"是否可能为空"成为签名的一部分,比 Java 清晰。


8.4 Callable:函数的类型

Java 用 Function<T,R>/Consumer<T>/Predicate<T>。Python 用 Callable

from collections.abc import Callable

# 一个接受两个 int、返回 int 的函数
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

apply(lambda x, y: x + y, 1, 2)

不加参数的 Callable[..., int] 表示"任意参数、返回 int"。


8.5 泛型(TypeVar

Java 的 <T>。Python 用 TypeVar

  • Java

    <T> T first(List<T> list) {
        return list.get(0);
    }
    
  • Python

    from typing import TypeVar
    
    T = TypeVar("T")
    
    def first(items: list[T]) -> T:
        return items[0]
    

有界泛型(bound,对应 Java <T extends Comparable>):

from typing import TypeVar
from numbers import Real

N = TypeVar("N", bound=Real)        # N 必须是 Real 的子类
def add(a: N, b: N) -> N: ...

8.6 Protocol:结构化类型(重点)⭐

这是 Python 类型系统相对 Java 最有意思的地方。回顾第 4 章:Java 的 interfacenominal(必须显式 implements),Python 的 Protocol结构化的——有对应方法就算符合,不需要声明

from typing import Protocol

class Quackable(Protocol):
    def quack(self) -> str: ...

def make_sound(x: Quackable) -> str:
    return x.quack()

class Duck:                       # 不写 (Quackable)
    def quack(self) -> str: return "quack"

make_sound(Duck())               # ✅ mypy 认可:结构匹配
  • Java nominal

    interface Quackable { String quack(); }
    // 第三方类无法让它 implements Quackable
    
  • Python structural

    class Quackable(Protocol): ...
    # 任何有 quack() 的对象自动符合,
    # 包括你改不了的第三方类
    

核心认知

Protocol = "鸭子类型 + 静态检查"。你拿到 Java interface 的类型安全,却不用到处 implements,还能适配改不了的第三方类。这是 Python 类型系统比 Java 更灵活的体现。

运行时可检查的 Protocol@runtime_checkable)支持 isinstance,但只检查方法是否存在、不检查签名。


8.7 其他常用类型工具

from typing import Any, Final, Literal, NoReturn

config: Any = ...                  # 关闭类型检查(逃生舱,慎用)
MAX_SIZE: Final[int] = 100         # 常量(类似 Java final)
def http_method() -> Literal["GET", "POST"]: ...   # 字面值类型
def fail(msg: str) -> NoReturn:    # 表示函数永不正常返回(如 sys.exit)
    raise SystemExit(msg)

# 类型别名(3.12+ 用 type 语句)
type Vector = list[float]
def norm(v: Vector) -> float: ...

Literal 很有用:Literal["r", "w"] 限定文件打开模式,比 Java 的枚举更轻。


8.8 渐进式采用策略

类型提示是渐进式(gradual)的——你可以:

  1. 先给公共 API 加注解(函数签名),内部实现留 Any
  2. 从关键/复杂模块开始,逐步铺开。
  3. 配合 mypy/pyright 在 CI 里跑。

mypy 配置(写在 pyproject.toml):

[tool.mypy]
python_version = "3.13"
strict = true              # 最严格(可选,按团队接受度调)
warn_return_any = true
disallow_untyped_defs = true
mypy .                     # 检查整个项目

mypy vs pyright

  • mypy:Python 官方生态、成熟、命令行友好。
  • pyright(微软,PyCharm/VSCode 背后的引擎):更快、IDE 集成好、推断更智能。
  • 日常开发靠 IDE 实时提示(PyCharm 内置、VSCode 装 Pylance),CI 里跑 mypy。

本章练习

练习 8.1

给下面函数加上精确的类型注解:接收一个字符串列表,返回一个"按首字母分组的字典"(dict[str, list[str]]),键是小写首字母。

参考答案
def group_by_initial(words: list[str]) -> dict[str, list[str]]:
    result: dict[str, list[str]] = {}
    for w in words:
        key = w[0].lower()
        result.setdefault(key, []).append(w)
    return result
练习 8.2

Protocol 定义一个 Closeable(有 close(self) -> None),写一个泛型函数 with_resource[T](x: T) -> T(用 TypeVar)调用它的 close。思考:为什么这里用 Protocol 而不是 ABC?

参考答案

from typing import Protocol, TypeVar

class Closeable(Protocol):
    def close(self) -> None: ...

T = TypeVar("T", bound=Closeable)

def with_resource(x: T) -> T:
    x.close()
    return x
用 Protocol 是因为 close() 常见于各种第三方对象(文件、连接),它们没有共同基类。结构化类型让 with_resource 能接受任何有 close 的对象,无需它们继承某个 ABC。

练习 8.3

解释 age: int = 30 之后 age = "thirty" 为何运行时不报错,以及如何让它"被发现"。

参考答案

类型注解只是元数据,解释器运行时不校验,故赋字符串能跑。要让错误暴露,需用静态检查器:mypy yourfile.py 会报 error: Incompatible types in assignment (expression has type "str", variable has type "int")。IDE(Pylance/PyCharm)也会实时标红。


上一章:第 7 章 · Pythonic 惯用法 | 下一章:第 9 章 · 标准库对照速查