python从入门到精通-第8章: 类型系统 — Python的类型注解革命

第8章: 类型系统 --- Python的类型注解革命

Java/Kotlin 开发者习惯了编译器在类型上兜底,初看 Python 会觉得"这语言没类型"。错。Python 3.10+ 的类型注解体系已经非常成熟,PEP 484/526/612/695/696 一路演进,加上 mypy/pyright 等静态检查器,你可以在 Python 中获得接近 Java 的类型安全体验------但本质不同:Python 的类型注解是可选的、运行时默认忽略的、由外部工具检查的。理解这个本质差异,是本章的核心。


8.1 类型注解基础

Java/Kotlin 对比

java 复制代码
// Java: 类型是语言的一部分,编译器强制执行
public class User {
    private String name;           // 必须声明类型
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String greet() {        // 返回类型必须声明
        return "Hi, " + this.name;
    }
}
// 类型错误 → 编译失败,一行代码都跑不了
kotlin 复制代码
// Kotlin: 类型推断减少样板,但仍然是编译期强制
class User(val name: String, val age: Int) {
    fun greet(): String = "Hi, $name"   // 返回类型可推断但推荐声明
}
// val x: Int = "hello"  // 编译错误!

Python 实现

python 复制代码
# Python: 类型注解是"提示",运行时默认不检查
def greet(name: str, age: int) -> str:
    return f"Hi, {name}, age {age}"

# 运行时完全正常------Python 不在乎你传了什么
result = greet("Alice", 30)       # 正常
result2 = greet("Bob", "thirty")  # 也正常!运行时不会报错
result3 = greet(42, None)         # 照样跑!f-string 不会抛异常

# 但类型检查器会报警
# mypy 会报: Argument 2 to "greet" has incompatible type "str"; expected "int"


# === 变量注解 ===
name: str = "Alice"
age: int = 30
scores: list[float] = [98.5, 87.3, 95.0]

# 注解但不赋值------运行时不会创建变量
address: str  # 只是告诉类型检查器"这个变量将来会是 str"
# print(address)  # NameError! 变量根本不存在


# === 类注解 ===
class User:
    name: str           # 类属性注解(不创建实例属性)
    age: int

    def __init__(self, name: str, age: int) -> None:
        self.name = name    # 实例属性赋值
        self.age = age

    def greet(self) -> str:
        return f"Hi, {self.name}"


# === typing.get_type_hints(): 运行时获取注解 ===
from typing import get_type_hints

class Config:
    host: str
    port: int
    debug: bool

    def __init__(self, host: str, port: int = 8080) -> None:
        self.host = host
        self.port = port

# 获取函数的类型注解
hints = get_type_hints(Config.__init__)
print(hints)
# {'host': <class 'str'>, 'port': <class 'int'>, 'return': <class 'None'>}

# 获取类的类型注解
class_hints = get_type_hints(Config)
print(class_hints)
# {'host': <class 'str'>, 'port': <class 'int'>, 'debug': <class 'bool'>}

# 直接访问 __annotations__ 也可以(但 get_type_hints 会处理前向引用)
print(Config.__annotations__)
# {'host': <class 'str'>, 'port': <class 'int'>, 'debug': <class 'bool'>}

核心差异

维度 Java/Kotlin Python
类型检查时机 编译期,强制 运行时默认忽略,mypy/pyright 外部检查
类型错误后果 编译失败,无法运行 正常运行,类型检查器报警
注解本质 语言语法的一部分 特殊属性 __annotations__,本质是字典
性能影响 零(编译期处理) 微小(注解存储在字典中)
前向引用 编译器自动处理 需要字符串引号或 from __future__ import annotations

常见陷阱

python 复制代码
# 陷阱1: 注解不是类型约束!
x: int = "hello"  # 运行时完全正常,mypy 才会报错

# 陷阱2: 类属性注解 vs 实例属性
class Bad:
    name: str  # 这是类属性注解,不是实例属性!

b = Bad()
# print(b.name)  # AttributeError! 没有通过 __init__ 赋值

# 陷阱3: 前向引用需要引号(Python 3.10)
class Node:
    def __init__(self, value: int, next: "Node | None" = None) -> None:
        self.value = value
        self.next = next
# Python 3.11+ 可以用 from __future__ import annotations 避免引号

# 陷阱4: __annotations__ 不包含函数参数默认值的类型推导
def foo(x=42):
    pass
print(foo.__annotations__)  # {} --- 没有注解就没有

何时使用

  • 必须用: 公共 API、团队协作项目、数据处理管道
  • 推荐用: 任何超过 200 行的文件
  • 可以不用: 一次性脚本、Jupyter notebook 探索性分析

8.2 typing 核心: Optional, Union, Literal, Any, NoReturn

Java/Kotlin 对比

java 复制代码
// Java: Optional<T> 是一个包装容器,防止 null
import java.util.Optional;

public class UserService {
    // 返回 Optional 表示可能找不到
    public Optional<User> findUser(String name) {
        return Optional.ofNullable(db.get(name));
    }

    // 参数用 @Nullable 注解(非标准,需 lombok/checker framework)
    public void updateName(@Nullable String newName) {
        if (newName != null) {
            // ...
        }
    }
}
// Java 的 null 是一等公民,Optional 只是包装
kotlin 复制代码
// Kotlin: ? 是语言内置的可空类型,编译器强制 null 检查
fun findUser(name: String): User? {  // ? 表示可为 null
    return db[name]
}

val user = findUser("Alice")
println(user?.name)         // 安全调用
println(user?.name ?: "N/A") // Elvis 运算符
// println(user.name)       // 编译错误!必须先处理 null

Python 实现

python 复制代码
from typing import Optional, Union, Literal, Any, NoReturn


# === Optional[X] 等价于 Union[X, None] ===
def find_user(name: str) -> Optional[str]:
    """找不到返回 None"""
    users = {"Alice": "alice@example.com", "Bob": "bob@example.com"}
    return users.get(name)  # dict.get 找不到返回 None


email = find_user("Alice")   # str | None
email2 = find_user("Eve")    # None

# Python 不会强制你处理 None!
# print(email2.upper())      # 运行时 AttributeError!mypy 会提前警告


# === Union: 多种类型之一 ===
def process(value: Union[int, str, float]) -> str:
    """接受 int、str 或 float"""
    if isinstance(value, int):
        return f"integer: {value}"
    elif isinstance(value, str):
        return f"string: {value}"
    else:
        return f"float: {value}"


# Python 3.10+ 可以用 | 语法代替 Union
def process_modern(value: int | str | float) -> str:
    if isinstance(value, int):
        return f"integer: {value}"
    elif isinstance(value, str):
        return f"string: {value}"
    else:
        return f"float: {value}"

# Optional 也可以用 | 语法
def find_user_modern(name: str) -> str | None:
    return {"Alice": "alice@example.com"}.get(name)


# === Literal: 限定为特定字面值 ===
def set_mode(mode: Literal["debug", "info", "error"]) -> None:
    print(f"Mode set to: {mode}")

set_mode("debug")   # OK
set_mode("info")    # OK
# set_mode("warn")  # mypy 报错!不在 Literal 列表中
# set_mode(42)      # mypy 报错!

# 组合 Literal 和 Union
def configure(key: Literal["host", "port"], value: str | int) -> None:
    print(f"Setting {key} = {value}")


# === Any: 放弃类型检查 ===
from typing import Any

def process_any(data: Any) -> Any:
    """Any 是顶级类型,任何值都能赋给它,它也能赋给任何类型"""
    return data

x: int = process_any("hello")  # mypy 不报错!Any 会"污染"类型
# 对比 Java: Object 也有类似问题,但 Kotlin 的 Any 不会隐式赋值给 String


# === NoReturn: 标记永远不会正常返回的函数 ===
import sys

def fail(message: str) -> NoReturn:
    """抛异常或退出进程,永远不会 return"""
    print(f"FATAL: {message}")
    sys.exit(1)

def assert_positive(value: int) -> None:
    if value < 0:
        fail(f"Expected positive, got {value}")
    # mypy 知道 fail() 不会返回,所以这里不需要 else 分支
    print(f"OK: {value}")


# === 实战: 用 Union + Literal 建模配置 ===
from typing import TypedDict

class ServerConfig(TypedDict):
    host: str
    port: Literal[80, 443, 8080, 8443]
    ssl: bool
    db: Union[str, None]  # 数据库连接字符串,可选

config: ServerConfig = {
    "host": "localhost",
    "port": 8080,
    "ssl": False,
    "db": None,
}

核心差异

概念 Java/Kotlin Python
可空 Kotlin T? / Java @Nullable Optional[T] 或 `T
多类型 Java 无原生支持 Union[A, B] 或 `A
字面值约束 无原生支持 Literal["a", "b"]
顶级类型 Object / Any Any(会污染类型)
不返回 无对应概念 NoReturn

常见陷阱

python 复制代码
# 陷阱1: Optional 不会帮你处理 None
def get_length(s: Optional[str]) -> int:
    return len(s)  # mypy 报错!s 可能是 None
    # 正确: return len(s) if s else 0

# 陷阱2: Any 会绕过所有类型检查------慎用
def bad(x: Any) -> int:
    return x  # mypy 不报错,但运行时可能返回任何东西

# 陷阱3: Union 的顺序不影响语义
Union[int, str] == Union[str, int]  # True --- 不是"优先匹配"

# 陷阱4: isinstance 检查后 mypy 能自动收窄类型(type narrowing)
def handle(value: int | str) -> int:
    if isinstance(value, str):
        return len(value)   # mypy 知道这里 value 是 str
    return value             # mypy 知道这里 value 是 int

何时使用

  • Optional / T | None: 函数可能不返回有效值时
  • Union: API 需要兼容多种输入时
  • Literal: 限定枚举值、状态机、配置项
  • Any: 遗留代码迁移、动态数据(JSON)的临时方案------尽快消除
  • NoReturn: 异常抛出函数、进程退出函数

8.3 泛型: TypeVar, Generic, Protocol

Java/Kotlin 对比

java 复制代码
// Java: 泛型在编译期检查,运行时擦除(type erasure)
public class Box<T> {
    private T value;
    public Box(T value) { this.value = value; }
    public T get() { return value; }
    public void set(T value) { this.value = value; }
}

// 运行时 Box<String> 和 Box<Integer> 都是 Box
Box<String> box = new Box<>("hello");
// box.getClass() == Box.class --- 看不到 String

// 泛型边界
public class NumberBox<T extends Number> {
    private T value;
    public double doubleValue() { return value.doubleValue(); }
}
kotlin 复制代码
// Kotlin: 泛型声明处型变(out/in)+ 类型投影
class Box<out T>(val value: T)  // 协变:只能读不能写
class MutableBox<T>(var value: T)  // 不变:可读可写

fun <T : Comparable<T>> maxOf(a: T, b: T): T =
    if (a >= b) a else b

// 星投影:不知道具体类型参数时
fun process(box: Box<*>) {
    val value: Any? = box.value  // 只能当 Any 用
}

Python 实现

python 复制代码
from typing import TypeVar, Generic, Protocol, TypeGuard
from collections.abc import Iterable


# === TypeVar: 定义类型变量 ===
T = TypeVar("T")                    # 无约束,可以是任何类型
N = TypeVar("N", int, float)        # 约束为 int 或 float(值类型约束)
C = TypeVar("C", bound="Comparable")  # 上界约束(类似 Java extends)


# === 泛型函数 ===
def first(lst: list[T]) -> T:
    """返回列表第一个元素"""
    return lst[0]

# mypy 推断: first([1, 2, 3]) -> int, first(["a", "b"]) -> str
x: int = first([1, 2, 3])
s: str = first(["hello", "world"])


def add(a: N, b: N) -> N:
    """两个数字相加"""
    return a + b  # type: ignore  # int + float 返回 float,mypy 可能报错

result: int = add(1, 2)
result2: float = add(1.5, 2.5)
# result3: str = add("a", "b")  # mypy 报错!N 只允许 int 或 float


# === 泛型类 ===
class Box(Generic[T]):
    """泛型容器"""
    def __init__(self, value: T) -> None:
        self._value = value

    def get(self) -> T:
        return self._value

    def set(self, value: T) -> None:
        self._value = value


int_box: Box[int] = Box(42)
str_box: Box[str] = Box("hello")

# mypy 检查类型一致性
int_box.set(100)
# int_box.set("hello")  # mypy 报错!
val: int = int_box.get()


# === 多类型参数 ===
K = TypeVar("K")
V = TypeVar("V")

class Pair(Generic[K, V]):
    def __init__(self, key: K, value: V) -> None:
        self.key = key
        self.value = value

pair: Pair[str, int] = Pair("age", 30)


# === Protocol: 结构化子类型(鸭子类型的静态版本)===
class Drawable(Protocol):
    """任何有 draw() 方法的类都自动满足此协议"""
    def draw(self) -> str: ...


class Circle:
    def draw(self) -> str:
        return "○"


class Rectangle:
    def draw(self) -> str:
        return "□"


class NotDrawable:
    def render(self) -> str:
        return "???"


def render_all(items: list[Drawable]) -> None:
    for item in items:
        print(item.draw())


# Circle 和 Rectangle 自动满足 Drawable,无需继承!
render_all([Circle(), Rectangle()])
# render_all([NotDrawable()])  # mypy 报错!没有 draw() 方法

# 这就是 Python 的"静态鸭子类型"------vs Java 必须显式 implements


# === Protocol 带属性 ===
class Named(Protocol):
    name: str  # 协议可以要求属性


class Person:
    def __init__(self, name: str) -> None:
        self.name = name


class Company:
    def __init__(self, name: str) -> None:
        self.name = name


def get_name(entity: Named) -> str:
    return entity.name

# Person 和 Company 都自动满足 Named
print(get_name(Person("Alice")))   # Alice
print(get_name(Company("Google"))) # Google


# === TypeGuard: 自定义类型收窄 ===
from typing import TypeGuard

def is_int_list(val: list) -> TypeGuard[list[int]]:
    """告诉 mypy: 如果返回 True,val 就是 list[int]"""
    return all(isinstance(x, int) for x in val)

def process_numbers(items: list[int | str]) -> None:
    if is_int_list(items):
        # mypy 知道这里 items 是 list[int]
        total = sum(items)  # OK
        print(f"Sum: {total}")

核心差异

维度 Java/Kotlin Python
泛型实现 编译期检查,运行时擦除 纯运行时属性,mypy 检查
类型约束 extends / : bound= 参数
协变/逆变 out T / in T(声明处) covariant=True / contravariant=True
结构化类型 无(必须显式 implements) Protocol(鸭子类型的静态版)
泛型实例 运行时不可见 运行时可通过 __orig_class__ 查看

常见陷阱

python 复制代码
# 陷阱1: TypeVar 名字和变量名必须相同
T = TypeVar("T")   # 正确
# X = TypeVar("T")  # 能用但极度混乱,mypy 警告

# 陷阱2: Generic 必须显式继承
class BadBox:  # 不是泛型类!
    def __init__(self, value: T) -> None:  # T 在这里只是个全局变量
        self._value = value

class GoodBox(Generic[T]):  # 正确
    def __init__(self, value: T) -> None:
        self._value = value

# 陷阱3: Protocol 不能用于 isinstance 检查(运行时)
# isinstance(Circle(), Drawable)  # TypeError!
# 用 @runtime_checkable 装饰器可以启用,但有性能开销
from typing import runtime_checkable

@runtime_checkable
class CheckableDrawable(Protocol):
    def draw(self) -> str: ...

print(isinstance(Circle(), CheckableDrawable))  # True

何时使用

  • TypeVar: 函数/类需要保持输入输出类型一致时
  • Generic: 需要类型参数化的容器类
  • Protocol: 定义接口但不强制继承关系时------Python 的杀手特性
  • TypeGuard: 自定义类型收窄逻辑时

8.4 PEP 695 (3.12+): 新泛型语法

Java/Kotlin 对比

java 复制代码
// Java: 泛型参数在方法名前声明
public <T> T first(List<T> list) {
    return list.get(0);
}

// 泛型参数在类名后声明
public class Box<T> {
    private T value;
    public Box(T value) { this.value = value; }
    public T get() { return value; }
}
kotlin 复制代码
// Kotlin: 泛型参数在 fun/class 关键字后声明
fun <T> first(lst: List<T>): T = lst.first()

class Box<T>(val value: T) {
    fun get(): T = value
}

// 泛型默认值(Kotlin 不支持)
// Java 也不支持泛型参数默认值

Python 实现

python 复制代码
# === PEP 695: 全新泛型语法(Python 3.12+)===
# 不再需要 from typing import TypeVar, Generic


# === 泛型函数: 类型参数在方括号中声明 ===
def first[T](lst: list[T]) -> T:
    """返回列表第一个元素"""
    return lst[0]

# mypy 自动推断类型参数
x: int = first([1, 2, 3])
s: str = first(["hello"])


# === 泛型类: 类型参数在类名后声明 ===
class Box[T]:
    def __init__(self, value: T) -> None:
        self._value = value

    def get(self) -> T:
        return self._value

    def set(self, value: T) -> None:
        self._value = value

int_box: Box[int] = Box(42)
str_box: Box[str] = Box("hello")


# === 多类型参数 ===
class Pair[K, V]:
    def __init__(self, key: K, value: V) -> None:
        self.key = key
        self.value = value

pair: Pair[str, int] = Pair("age", 30)


# === 类型参数约束 ===
def add[T: (int, float)](a: T, b: T) -> T:
    """T 必须是 int 或 float"""
    return a + b  # type: ignore

# 对比旧语法: T = TypeVar("T", int, float)
# 新语法用冒号+元组,更简洁


# === 类型参数上界 ===
class Container[T: object]:
    """T 必须是 object 的子类型(几乎所有类型都满足)"""
    def __init__(self, value: T) -> None:
        self._value = value


# === 类型参数默认值(3.13+)===
# Python 3.13 起支持泛型参数默认值
class Result[T, E: Exception = Exception]:
    """类似 Rust 的 Result<T, E>,E 默认为 Exception"""
    def __init__(self, value: T | None = None, error: E | None = None) -> None:
        self._value = value
        self._error = error

    def is_ok(self) -> bool:
        return self._error is None

    def unwrap(self) -> T:
        if self._error is not None:
            raise self._error
        return self._value  # type: ignore


ok: Result[int] = Result(value=42)       # E 默认为 Exception
err: Result[int, ValueError] = Result(error=ValueError("bad"))


# === 新旧语法对比 ===
# 旧(3.11 及之前):
from typing import TypeVar, Generic

T_old = TypeVar("T_old")

def first_old(lst: list[T_old]) -> T_old:
    return lst[0]

class Box_old(Generic[T_old]):
    def __init__(self, value: T_old) -> None:
        self._value = value

# 新(3.12+):
def first_new[T](lst: list[T]) -> T:
    return lst[0]

class Box_new[T]:
    def __init__(self, value: T) -> None:
        self._value = value

# 新语法的优势:
# 1. 不需要 import TypeVar/Generic
# 2. 类型参数作用域限定在函数/类内(不会污染全局)
# 3. 语法更接近 Java/Kotlin,JVM 开发者更直观

核心差异

维度 Java/Kotlin Python 3.12+
泛型函数声明 <T> def first(...) / fun <T> first(...) def first[T](...)
泛型类声明 class Box<T> / class Box<T> class Box[T]
约束语法 T extends Number / T : Number T: (int, float)T: bound
默认类型参数 不支持 3.13+ 支持
作用域 类/方法内 类/函数内(新语法),全局(旧语法)

常见陷阱

python 复制代码
# 陷阱1: 新语法需要 Python 3.12+,3.10/3.11 只能用旧语法
# def first[T](lst: list[T]) -> T:  # 3.11 及之前 SyntaxError!

# 陷阱2: 新旧语法不能混用
T = TypeVar("T")
# def foo[T, U](x: T, y: U) -> T:  # SyntaxError! 不能同时用两种语法

# 陷阱3: 类型参数默认值需要 3.13+
# class Box[T = int]:  # 3.12 会 SyntaxError

何时使用

  • Python 3.12+: 优先使用新语法,更简洁、作用域更清晰
  • Python 3.10/3.11: 只能用旧语法(TypeVar + Generic)
  • 跨版本兼容: 用旧语法,或 typing_extensions

8.5 type 语句 (3.12+): 类型别名

Java/Kotlin 对比

java 复制代码
// Java: 没有类型别名(直到最近才有 preview)
// 变通方案: 继承或包装
public class UserID extends String {  // 不合法!Java 不支持
}

// Java 21 preview: 类型别名(尚未稳定)
// typealias UserID = String;
kotlin 复制代码
// Kotlin: typealias 创建类型别名
typealias UserID = String
typealias Point = Pair<Double, Double>
typealias Handler = (Event) -> Unit

fun lookup(id: UserID): User? { ... }
// UserID 在编译后就是 String,但提供了语义信息

Python 实现

python 复制代码
# === type 语句: 创建类型别名(Python 3.12+)===

# 基本类型别名
type UserID = str
type Point = tuple[float, float]
type Score = int

def lookup(user_id: UserID) -> Point:
    """UserID 和 Point 都是类型别名,运行时就是 str 和 tuple"""
    return (0.0, 0.0)

uid: UserID = "alice-001"
pos: Point = (1.0, 2.0)
score: Score = 95

# mypy 会用别名名显示类型,提高可读性


# === 泛型类型别名 ===
type Matrix[T] = list[list[T]]
type Mapping[K, V] = dict[K, V]
type Result[T] = tuple[bool, T | None]

int_matrix: Matrix[int] = [[1, 2], [3, 4]]
str_matrix: Matrix[str] = [["a", "b"], ["c", "d"]]
name_map: Mapping[str, int] = {"Alice": 30, "Bob": 25}
result: Result[str] = (True, "success")


# === 复杂类型的可读别名 ===
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
type Headers = dict[str, str]
type Handler = Callable[[str], bool]

# 对比不用别名的函数签名:
# def process(data: dict[str, dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None], headers: dict[str, str]) -> None:
#     ...

def process(data: JSON, headers: Headers) -> None:
    """类型别名让签名清晰可读"""
    pass


# === 旧语法: TypeAlias ===
# Python 3.10/3.11 需要用赋值或 TypeAlias
from typing import TypeAlias

PointOld: TypeAlias = tuple[float, float]
UserIDOld: TypeAlias = str

# 或者直接赋值(不推荐,可读性差)
PointOld2 = tuple[float, float]  # mypy 能识别,但不够明确


# === 递归类型别名 ===
# JSON 是递归定义的:对象值可以是 JSON 本身
type JSONObject = dict[str, "JSONValue"]
type JSONValue = str | int | float | bool | None | JSONObject | list["JSONValue"]

# 注意: 递归别名需要用前向引用(引号包裹)


# === type 语句 vs 赋值 ===
# type 语句是显式的类型别名声明
type X = int | str       # 显式,mypy/pyright 明确知道这是别名
Y = int | str            # 隐式,也能工作但语义不够清晰

# type 语句的优势:
# 1. 意图明确------一眼就知道这是类型别名
# 2. 支持泛型参数: type Matrix[T] = ...
# 3. 不能用作普通变量: type X = int; X = 42  # 赋值后 X 不再是类型别名

核心差异

维度 Java/Kotlin Python 3.12+
语法 typealias X = Y type X = Y
泛型别名 typealias Matrix<T> = List<List<T>> type Matrix[T] = list[list[T]]
递归别名 支持 支持(需前向引用)
运行时行为 编译后完全擦除 运行时是原类型的别名
旧版兼容 N/A TypeAlias 注解或直接赋值

常见陷阱

python 复制代码
# 陷阱1: type 语句创建的别名在运行时就是原类型
type UserID = str
uid: UserID = "alice"
print(type(uid))       # <class 'str'> --- 不是 UserID
print(isinstance(uid, str))  # True

# 陷阱2: type 语句需要 3.12+
# type Point = tuple[float, float]  # 3.11 及之前 SyntaxError

# 陷阱3: 别名不提供运行时类型安全
type UserID = str
uid: UserID = "alice"
uid2: UserID = 123     # mypy 报错,但运行时不检查

何时使用

  • 复杂联合类型需要命名以提高可读性
  • 领域概念需要类型级别的语义(如 UserID vs str
  • 泛型容器类型需要参数化别名(如 Matrix[int]

8.6 ParamSpec 与 TypeVarTuple: 高级泛型

Java/Kotlin 对比

java 复制代码
// Java: 泛型通配符处理未知类型参数
public interface Function<T, R> {
    R apply(T t);
}

// 通配符: ? extends T (上界), ? super T (下界)
public double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
    return total;
}

// Java 无法捕获整个函数签名(参数个数+类型)
kotlin 复制代码
// Kotlin: 星投影 * 表示未知类型参数
fun process(list: List<*>) {
    // 只能当 Any? 读取
}

// reified 类型参数(内联函数特化)
inline fun <reified T> typeName(): String = T::class.simpleName ?: "Unknown"
// Java 的泛型在运行时擦除,Kotlin 的 reified 通过内联绕过

Python 实现

python 复制代码
from typing import (
    ParamSpec, TypeVarTuple, Concatenate,
    Callable, Any
)
from collections.abc import Callable as AbcCallable


# === ParamSpec: 捕获函数的完整参数签名 ===
P = ParamSpec("P")
R = TypeVar("R")


def log_calls(func: Callable[P, R]) -> Callable[P, R]:
    """装饰器: 记录函数调用,ParamSpec 保留原始参数签名"""
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper


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

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

# mypy 知道 add 仍然是 (a: int, b: int) -> int
# greet 仍然是 (name: str, greeting: str = "Hello") -> str
result: int = add(1, 2)
# 输出:
# Calling add with args=(1, 2), kwargs={}
# add returned 3


# === Concatenate: 在参数签名前添加参数 ===
def with_retry(
    func: Callable[P, R],
    max_retries: int = 3
) -> Callable[Concatenate[int, P], R]:
    """
    装饰器工厂: 在原函数参数前添加一个 timeout 参数
    Concatenate[int, P] 表示 (int, *原始参数)
    """
    def wrapper(timeout: int, *args: P.args, **kwargs: P.kwargs) -> R:
        last_error: Exception | None = None
        for attempt in range(max_retries):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                last_error = e
                print(f"Attempt {attempt + 1} failed: {e}")
        raise last_error  # type: ignore
    return wrapper


@with_retry(max_retries=5)
def fetch_data(url: str) -> str:
    """被装饰后签名变成 (timeout: int, url: str) -> str"""
    return f"Data from {url}"

# mypy 知道 fetch_data 现在需要 timeout 参数
data: str = fetch_data(timeout=10, url="https://example.com")


# === TypeVarTuple: 可变长度泛型 ===
Ts = TypeVarTuple("Ts")


def flatten(*arrays: *Ts) -> list:
    """将多个列表展平为一个列表"""
    result: list = []
    for arr in arrays:
        result.extend(arr)
    return result

result = flatten([1, 2], [3, 4], [5, 6])
# result: [1, 2, 3, 4, 5, 6]


# === 实战: 类型安全的矩阵运算 ===
import numpy as np
from typing import TypeVarTuple

T = TypeVar("T")
Ts = TypeVarTuple("Ts")

# TypeVarTuple 可以捕获任意长度的类型参数
# 例如: tuple[*Ts] 可以匹配 tuple[int], tuple[int, str], tuple[int, str, float] 等


# === 实战: 通用日志装饰器 ===
def create_logger(prefix: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """工厂函数返回装饰器,ParamSpec 保留签名"""
    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            print(f"[{prefix}] {func.__name__} called")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@create_logger("API")
def get_user(user_id: int, detailed: bool = False) -> dict[str, str]:
    return {"id": str(user_id), "detail": "full" if detailed else "basic"}

# mypy 知道 get_user 仍然是 (user_id: int, detailed: bool) -> dict
info: dict[str, str] = get_user(42, detailed=True)

ParamSpec 实战: 装饰器保留函数签名

python 复制代码
from typing import ParamSpec, TypeVar, Callable
from functools import wraps

P = ParamSpec('P')  # 捕获函数的参数签名
R = TypeVar('R')     # 捕获返回类型

def log_calls(func: Callable[P, R]) -> Callable[P, R]:
    """装饰器: 记录函数调用,完整保留原函数的签名"""
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f"调用 {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"返回: {result}")
        return result
    return wrapper

# 使用: 类型检查器知道 greet 的签名是 (name: str, times: int) -> str
@log_calls
def greet(name: str, times: int = 1) -> str:
    return f"Hello, {name}! " * times

# mypy/pyright 能正确推断:
result: str = greet("Alice", times=3)
# greet(123)  # mypy 报错: Expected str, got int

# === Concatenate: 在参数前添加参数 ===
from typing import Concatenate

# 添加一个 db 参数到所有方法
def with_db(func: Callable[Concatenate[dict, P], R]) -> Callable[P, R]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        db = {"connection": "active"}  # 模拟数据库连接
        return func(db, *args, **kwargs)
    return wrapper

@with_db
def get_user(db: dict, user_id: int) -> str:
    return f"User {user_id} from {db['connection']}"

# 类型检查器知道 get_user 的签名是 (user_id: int) -> str
# db 参数被 with_db 自动注入
result = get_user(42)  # mypy 正确!
print(result)  # User 42 from active

TypeVarTuple 实战: 可变长度泛型

python 复制代码
from typing import TypeVarTuple

Ts = TypeVarTuple('Ts')

# 定义一个可以接受任意数量类型的数组
class Array:
    def __init__(self, *items):
        self.items = items

    def __repr__(self):
        return f"Array({self.items})"

# TypeVarTuple 用于矩阵等数学类型
def flatten(arrays: list[*Ts]) -> list[*Ts]:
    """展平嵌套列表 --- TypeVarTuple 保留元素类型"""
    result = []
    for arr in arrays:
        result.extend(arr)
    return result

# 实际应用: batched 函数 (3.12+ 内置)
# Python 3.12 之前需要手动实现
from itertools import islice

def batched(iterable, n):
    """将可迭代对象按 n 个一组分割"""
    it = iter(iterable)
    while batch := tuple(islice(it, n)):
        yield batch

data = list(range(10))
for batch in batched(data, 3):
    print(batch)
# (0, 1, 2)
# (3, 4, 5)
# (6, 7, 8)
# (9,)

核心差异

维度 Java/Kotlin Python
捕获参数签名 不支持 ParamSpec
添加参数到签名 不支持 Concatenate
可变长度泛型 不支持 TypeVarTuple
通配符 ? extends T / * ParamSpec 更强大
reified Kotlin inline reified Python 天然 reified(运行时有类型)

常见陷阱

python 复制代码
# 陷阱1: ParamSpec 只能用于 Callable 的参数部分
P = ParamSpec("P")
# def bad(x: P) -> None:  # TypeError! P 只能和 Callable 一起用

# 陷阱2: Concatenate 的第一个参数必须是具体类型
from typing import Concatenate
# def bad(func: Callable[Concatenate[P, int], R]):  # 错误!第一个必须是具体类型

# 陷阱3: TypeVarTuple 用 * 解包
Ts = TypeVarTuple("Ts")
# def foo(args: Ts): ...     # 错误!需要 * 解包
def foo(*args: *Ts): ...     # 正确

何时使用

  • ParamSpec: 编写保留原函数签名的装饰器时------非常实用
  • Concatenate: 装饰器需要添加额外参数时
  • TypeVarTuple: 处理可变长度类型参数(如矩阵、张量)时

8.7 TypedDict, NamedTuple: 结构化类型

Java/Kotlin 对比

java 复制代码
// Java Record (Java 16+): 不可变数据载体
public record Point(double x, double y) {}
// 自动生成: constructor, getX(), getY(), equals(), hashCode(), toString()

// Java 没有直接的字典结构类型------通常用 Map<String, Object>
Map<String, Object> config = Map.of(
    "host", "localhost",
    "port", 8080
);
// 没有类型安全------值都是 Object
kotlin 复制代码
// Kotlin data class: 不可变数据载体
data class Point(val x: Double, val y: Double)
// 自动生成: constructor, component1(), component2(), copy(), equals(), hashCode(), toString()

// Map 没有结构约束
val config = mapOf(
    "host" to "localhost",
    "port" to 8080
)
// 类型: Map<String, Any> --- 值类型丢失

Python 实现

python 复制代码
from typing import TypedDict, NamedTuple, NotRequired, Required


# === TypedDict: 给字典加上结构约束 ===
class User(TypedDict):
    name: str
    age: int
    email: str


# 创建 TypedDict 实例------本质还是 dict
user: User = {
    "name": "Alice",
    "age": 30,
    "email": "alice@example.com",
}

# mypy 会检查键和值的类型
print(user["name"])     # mypy 知道返回 str
# user["phone"] = "123" # mypy 报错!User 没有 phone 键
# user["age"] = "thirty" # mypy 报错!age 必须是 int

# TypedDict 就是 dict------所有 dict 操作都能用
for key, value in user.items():
    print(f"{key}: {value}")


# === NotRequired / Required: 可选键 ===
class ServerConfig(TypedDict):
    host: str
    port: int
    debug: NotRequired[bool]   # 可选键
    ssl: Required[bool]        # 显式必需(在 total=False 时有用)


config: ServerConfig = {
    "host": "localhost",
    "port": 8080,
    "ssl": True,
    # debug 可以省略
}


# === total=False: 所有键默认可选 ===
class PartialUser(TypedDict, total=False):
    name: str
    age: int
    email: str

# 用于 PATCH 请求------只传要更新的字段
update: PartialUser = {"age": 31}  # 只更新 age


# === 嵌套 TypedDict ===
class Address(TypedDict):
    city: str
    zip_code: str

class Employee(TypedDict):
    name: str
    address: Address

emp: Employee = {
    "name": "Bob",
    "address": {"city": "Beijing", "zip_code": "100000"},
}
# mypy 知道 emp["address"]["city"] 是 str


# === NamedTuple: 不可变的元组 + 字段名 ===
class Point(NamedTuple):
    x: float
    y: float

p = Point(1.0, 2.0)
print(p.x, p.y)          # 1.0 2.0 --- 像类一样访问
print(p[0], p[1])        # 1.0 2.0 --- 也像元组一样索引
# p.x = 3.0              # AttributeError! 不可变

# 自动生成 __repr__, ==, hash
print(p)                  # Point(x=1.0, y=2.0)
print(Point(1.0, 2.0) == Point(1.0, 2.0))  # True

# 支持解构
x, y = p
print(x, y)              # 1.0 2.0


# === NamedTuple 带默认值和方法 ===
class UserInfo(NamedTuple):
    name: str
    age: int
    email: str = "N/A"   # 默认值

    def is_adult(self) -> bool:
        return self.age >= 18

info = UserInfo("Alice", 30)
print(info.is_adult())   # True
print(info.email)        # N/A


# === NamedTuple vs TypedDict 选择 ===
# NamedTuple: 不可变、可哈希(可做 dict key)、位置访问
# TypedDict: 可变、键值访问、兼容 JSON 序列化


# === 实战: API 响应建模 ===
class APIError(TypedDict):
    code: int
    message: str
    details: NotRequired[str]

class APIResponse(TypedDict):
    success: bool
    data: NotRequired[dict[str, object]]
    error: NotRequired[APIError]

# 成功响应
ok_response: APIResponse = {
    "success": True,
    "data": {"id": 1, "name": "Alice"},
}

# 错误响应
err_response: APIResponse = {
    "success": False,
    "error": {"code": 404, "message": "Not found"},
}


# === 实战: 类型安全的配置 ===
from dataclasses import dataclass

# dataclass 是另一种选择------可变、有默认值、更灵活
@dataclass
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False

cfg = Config(host="0.0.0.0", port=3000)
cfg.debug = True  # 可变

核心差异

维度 Java Record Kotlin data class Python NamedTuple Python TypedDict
可变性 不可变 可变(var)/不可变(val) 不可变 可变
位置访问 componentN() componentN() 索引 不支持
命名访问 getX() .x .x ["x"]
可哈希 是(全 val 时)
JSON 兼容 需要库 需要库 需要转换 原生兼容
默认值 支持 支持 支持 不支持(用 NotRequired)

常见陷阱

python 复制代码
# 陷阱1: TypedDict 运行时不会检查类型
user: User = {"name": 42, "age": "thirty", "email": []}  # 运行时正常!
# 只有 mypy 会报错

# 陷阱2: TypedDict 不能用 kwargs 构造
# user = User(name="Alice", age=30)  # TypeError! 必须用字典字面量
user = {"name": "Alice", "age": 30, "email": "a@b.com"}  # 正确

# 陷阱3: NamedTuple 继承顺序
class Bad(NamedTuple, dict):  # 不要这样做
    x: int

# 陷阱4: TypedDict 的 NotRequired 和 total=False 的区别
class A(TypedDict):
    x: NotRequired[int]   # x 可以不存在

class B(TypedDict, total=False):
    x: int                # 所有键都可以不存在

class C(TypedDict):
    x: Required[int]      # x 必须存在(默认行为)

何时使用

  • TypedDict: JSON API 响应、配置字典、需要类型约束的字典
  • NamedTuple: 不可变数据记录、需要哈希的键、函数返回多值
  • dataclass: 需要可变性+默认值+方法时(第4章已详述)

8.8 typing.override (3.12+) 与其他装饰器

Java/Kotlin 对比

java 复制代码
// Java: @Override 注解------编译器检查是否真的覆盖了父类方法
public class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("Woof!");
    }

    // @Override
    // public void bark() {}  // 编译错误!Animal 没有 bark() 方法
}
// @Override 不是必须的,但强烈推荐------防止签名拼写错误
kotlin 复制代码
// Kotlin: override 是关键字------必须显式声明
open class Animal {
    open fun speak() { println("...") }
}

class Dog : Animal() {
    override fun speak() {  // 必须写 override!
        println("Woof!")
    }
}
// Kotlin 比 Java 更严格:不加 override 直接报错

Python 实现

python 复制代码
import typing
import warnings


# === typing.override (3.12+): 标记方法覆盖 ===
class Animal:
    def speak(self) -> str:
        return "..."

class Dog(Animal):
    @typing.override
    def speak(self) -> str:
        return "Woof!"

class Cat(Animal):
    @typing.override
    def speak(self) -> str:
        return "Meow!"

# mypy 检查: 如果父类没有 speak(),@typing.override 会报错
class Bad(Animal):
    @typing.override
    def bark(self) -> str:  # mypy 报错!Animal 没有 bark()
        return "Woof!"


# === typing.final: 防止覆盖和继承 ===
class BaseService:
    @typing.final
    def authenticate(self, token: str) -> bool:
        """子类不能覆盖此方法"""
        return token == "secret"

class UserService(BaseService):
    # @typing.override
    # def authenticate(self, token: str) -> bool:  # mypy 报错!final 方法不能覆盖
    #     return True

    def get_user(self, user_id: int) -> dict[str, str]:
        return {"id": str(user_id)}


@typing.final
class Singleton:
    """此类不能被继承"""
    pass

# class Sub(Singleton):  # mypy 报错!Singleton 是 final 的
#     pass


# === typing.deprecated (3.13+): 标记弃用 ===
# Python 3.13 起支持
# 3.10-3.12 可以用 warnings.warn() 手动处理

class OldAPI:
    @typing.deprecated("Use new_method() instead")
    def old_method(self) -> str:
        """弃用的方法------mypy 会报警"""
        warnings.warn(
            "old_method is deprecated, use new_method instead",
            DeprecationWarning,
            stacklevel=2,
        )
        return "old result"

    def new_method(self) -> str:
        return "new result"

api = OldAPI()
api.old_method()  # mypy 报弃用警告 + 运行时 DeprecationWarning


# === 实战: 框架基类设计 ===
class Controller:
    """Web 控制器基类"""

    @typing.final
    def handle_request(self, path: str) -> str:
        """模板方法------子类不能覆盖"""
        self.before_request()
        result = self.dispatch(path)
        self.after_request()
        return result

    def before_request(self) -> None:
        """钩子方法------子类可以覆盖"""
        pass

    @typing.override
    def dispatch(self, path: str) -> str:
        """子类必须覆盖"""
        raise NotImplementedError

    def after_request(self) -> None:
        """钩子方法------子类可以覆盖"""
        pass


class UserController(Controller):
    @typing.override
    def dispatch(self, path: str) -> str:
        if path == "/":
            return "User list"
        return "User detail"

    @typing.override
    def before_request(self) -> None:
        print("Authenticating...")


# === 3.10/3.11 兼容方案 ===
# 没有 typing.override,用 typing_extensions
# pip install typing_extensions
from typing_extensions import override, final

class AnimalCompat:
    def speak(self) -> str:
        return "..."

class DogCompat(AnimalCompat):
    @override
    def speak(self) -> str:
        return "Woof!"

核心差异

维度 Java @Override Kotlin override Python @typing.override
性质 注解(可选) 关键字(必须) 装饰器(可选)
检查时机 编译期 编译期 mypy/pyright
运行时效果 无(纯类型提示)
未覆盖父类方法 编译错误 编译错误 mypy 报错

常见陷阱

python 复制代码
# 陷阱1: @typing.override 运行时什么都不做
class Parent:
    pass

class Child(Parent):
    @typing.override
    def foo(self) -> None:  # 运行时正常!只有 mypy 报错
        pass

# 陷阱2: @typing.final 运行时也不阻止覆盖
class Parent:
    @typing.final
    def foo(self) -> None:
        pass

class Child(Parent):
    def foo(self) -> None:  # 运行时正常!只有 mypy 报错
        pass

# 陷阱3: 3.10/3.11 需要从 typing_extensions 导入
# from typing import override  # 3.11 及之前 ImportError
from typing_extensions import override  # 正确

何时使用

  • @override: 所有覆盖父类方法的地方------防止签名错误,提高可读性
  • @final: 框架/库中不允许子类修改的关键方法
  • @deprecated: API 迁移期标记旧接口

8.9 mypy/pyright 严格模式配置

Java/Kotlin 对比

java 复制代码
// Java: javac 默认就做类型检查
// 没有开关可以关闭------类型错误就是编译错误
// 可以用 -Xlint 增加额外警告
// javac -Xlint:all Main.java
kotlin 复制代码
// Kotlin: kotlinc 默认做类型检查
// 编译器选项控制严格程度
// kotlinc -Werror Main.kt  // 警告也当错误
// kotlinc -Xstrict Main.kt // 严格模式

Python 实现

python 复制代码
# === mypy: 最流行的 Python 类型检查器 ===
# 安装: pip install mypy

# 基本用法
# mypy script.py              # 检查单个文件
# mypy src/                   # 检查目录
# mypy --strict script.py     # 严格模式


# === mypy --strict 包含什么 ===
# --strict 等价于同时开启以下选项:
#
# --disallow-untyped-defs         函数必须有类型注解
# --disallow-any-generics         泛型不能用 Any
# --disallow-untyped-calls        不能调用无注解的函数
# --disallow-subclassing-any      不能继承 Any
# --warn-return-any               返回 Any 时警告
# --no-implicit-optional          None 不会自动成为默认值
# --strict-optional               Optional 严格检查
# --warn-redundant-casts          多余的类型转换警告
# --no-warn-no-return             无返回的函数必须标注 NoReturn
# --warn-unused-ignores           无用的 # type: ignore 警告
# --no-implicit-reexport          __init__.py 不隐式导出
# --strict-equality               == / != 类型检查
# --extra-checks                  额外检查


# === pyright: 微软出品的类型检查器(更快)===
# 安装: npm install -g pyright
# 或: pip install pyright

# 基本用法
# pyright script.py
# pyright --level standard src/


# === pyproject.toml 配置示例 ===
toml 复制代码
# pyproject.toml --- 推荐的项目级配置
[tool.mypy]
# 基本设置
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_any_generics = true

# 严格模式(渐进式开启)
strict_optional = true
strict_equality = true
check_untyped_defs = true

# 第三方库类型存根
# mypy 默认不检查无存根的第三方库
[[tool.mypy.overrides]]
module = [
    "requests.*",
    "numpy.*",
    "pandas.*",
]
ignore_missing_imports = true

# 测试代码可以放宽要求
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
warn_return_any = false

[tool.pyright]
# pyright 配置
pythonVersion = "3.10"
typeCheckingMode = "standard"
# typeCheckingMode 可选: "off", "basic", "standard", "strict"

# 排除目录
exclude = [
    "venv",
    ".venv",
    "build",
    "dist",
    "node_modules",
]

# 第三方库
venvPath = "."
venv = ".venv"
python 复制代码
# === 渐进式类型化策略 ===
# 不要一上来就开 strict------按阶段推进

# 阶段1: 基础注解(第1周)
# 只给公共 API 加注解
def public_api(user_id: int) -> dict[str, str]:
    """公共函数加注解"""
    return {"id": str(user_id)}

# 内部函数暂时不管
def _internal_helper(data):  # 暂时无注解
    return process(data)


# 阶段2: 启用 mypy 基础检查(第2-3周)
# mypy --disallow-untyped-defs src/
# 修复所有 "Missing type annotation" 错误


# 阶段3: 消除 Any(第4周)
# mypy --disallow-any-generics src/
# 把 List[Any] 改为 list[str] 等


# 阶段4: 严格模式(第5周+)
# mypy --strict src/
# 修复剩余的类型问题


# === # type: ignore 和 @overload ===
# 遗留代码或第三方库没有类型时,临时绕过

def legacy_function(x, y):
    # type: ignore  # mypy 跳过此函数的类型检查
    return x + y

# 更好的做法: 用 @overload 提供类型签名
from typing import overload

@overload
def process(value: int) -> int: ...
@overload
def process(value: str) -> str: ...
def process(value: int | str) -> int | str:
    """mypy 根据参数类型选择正确的重载签名"""
    if isinstance(value, int):
        return value * 2
    return value.upper()


# === 存根文件 (.pyi) ===
# 第三方库没有类型时,可以写 .pyi 存根
# mypy_stub_example.pyi
# def parse_json(text: str) -> dict[str, Any]: ...
# def format_date(dt: datetime) -> str: ...
# 存根文件和 .py 同目录,mypy 自动发现


# === pre-commit 集成 ===
# .pre-commit-config.yaml
# repos:
#   - repo: https://github.com/pre-commit/mirrors-mypy
#     rev: v1.8.0
#     hooks:
#       - id: mypy
#         args: [--strict]
#         additional_dependencies: [types-requests]

核心差异

维度 javac/kotlinc mypy/pyright
检查时机 编译期(必须通过) 开发期(可选工具)
配置方式 编译器参数 pyproject.toml / mypy.ini
渐进式采用 不支持 支持(逐文件/逐模块)
第三方库 有源码就有类型 需要存根 (.pyi) 或 types-xxx 包
运行时影响 零(纯静态分析)

常见陷阱

python 复制代码
# 陷阱1: mypy 默认不检查无存根的第三方库
import requests  # mypy 报: Library stubs not installed
# 解决: pip install types-requests
# 或在 pyproject.toml 中 ignore_missing_imports = true

# 陷阱2: --strict 对测试代码太严格
# 解决: 用 [[tool.mypy.overrides]] 给 tests/ 放宽

# 陷阱3: __init__.py 的隐式导出
# from package.module import func  # mypy 可能找不到
# 解决: 在 __init__.py 中显式导入,或设置 no_implicit_reexport = false

# 陷阱4: pyright 和 mypy 行为不完全一致
# 同一份代码可能一个报错一个不报------选择一个作为标准
# 推荐: 团队统一用 mypy(生态更成熟)或 pyright(VS Code 集成更好)

# 陷阱5: 动态特性无法类型化
values = [1, "hello", 3.14]  # 类型: list[int | str | float]
for v in values:
    # mypy 知道 v 是 int | str | float
    # 但无法知道循环第几次是什么类型
    pass

何时使用

  • 新项目 : 从第一天就加类型注解,用 --strict
  • 现有项目: 渐进式类型化,先公共 API 后内部代码
  • 库/框架: 必须有完整类型注解 + .pyi 存根
  • 脚本/工具: 至少给函数签名加注解
  • CI/CD: 集成 mypy 到 pre-commit 或 CI pipeline

总结: Python 类型系统的心智模型

markdown 复制代码
Java/Kotlin:  编译器强制类型 → 类型错误 = 编译失败
Python:       类型注解是提示 → mypy/pyright 检查 → 运行时不强制

关键原则:
1. 类型注解写给工具看,不是写给解释器看
2. 渐进式采用------从公共 API 开始
3. Protocol 是 Python 的杀手特性------鸭子类型 + 静态检查
4. 3.12+ 新语法(type 语句、泛型语法)大幅降低样板代码
5. 选择 mypy 或 pyright,不要两个混用
相关推荐
雨墨✘2 小时前
如何解决SQL多表查询数据重复问题_使用DISTINCT与JOIN优化
jvm·数据库·python
一战成名9962 小时前
把“看菜谱”变成“跟着做”:基于 Rokid 灵珠平台打造智能眼镜应用《厨房教练》
人工智能·python·rokid
小熊Coding2 小时前
Windows 上安装 mysqlclient 时遇到了编译错误,核心原因是缺少 Microsoft Visual C++ 14.0 或更高版本 的编译环境。
c++·windows·python·microsoft·django·mysqlclient·bug记录
好家伙VCC2 小时前
# BERT在中文文本分类中的实战优化:从基础模型到高效部署BERT(Bi
java·人工智能·python·分类·bert
u0107475462 小时前
JavaScript 递归调用栈深度解析与层级遍历陷阱详解
jvm·数据库·python
本地化文档2 小时前
requests-docs-l10n
python·http·github·gitcode
Metaphor6922 小时前
使用 Python 将 PowerPoint 转换为 PDF
python·pdf·powerpoint
XiYang-DING2 小时前
【Java】Lambda表达式
java·开发语言·python