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,不要两个混用
相关推荐
花酒锄作田6 小时前
[python]argparse 包在聊天机器人中的应用
python
NiceCloud喜云8 小时前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
AI玫瑰助手9 小时前
Python函数:默认参数的定义与注意事项
开发语言·python·信息可视化
weixin_468466859 小时前
全局与局部注意力机制新手实战指南
人工智能·python·深度学习·算法·自然语言处理·transformer·注意力机制
小糖学代码9 小时前
LLM系列:环境搭建:5.Python-dotenv 环境变量管理
人工智能·python·深度学习·神经网络
智慧物业老杨10 小时前
智慧物业合同周期管理系统:从风险预警到智能交接的全流程数智化落地方案
java·人工智能·python
橙橙笔记10 小时前
Python的学习第一部分
python·学习
voidmort10 小时前
3. 微调(Fine-tuning)与强化学习(RL)的核心思想
python·深度学习·算法
biter down11 小时前
基于 Pywinauto 的 QQ 音乐 GUI 自动化测试实践
python
人道领域11 小时前
【LeetCode刷题日记】669.修剪二叉搜索树
开发语言·python·算法