类型提示: 泛型
泛型第一块:TypeVar
========================================
TypeVar 是"类型占位符",用来表达"进去什么类型,出来就是什么类型"的关联。
from typing import TypeVar
T = TypeVar("T") # 声明一个类型变量 T,代表"某种类型,具体是啥先不定"
def first(items: list[T]) -> T:
"""
读法:传进来一个 list[T](元素是 T 类型),返回一个 T。
T 是同一个 —— 这就锁定了"元素类型 == 返回类型"。
"""
return items[0]
# 类型检查器会为每次调用【推断】出 T 具体是什么:
a = first([1, 2, 3]) # T 被推断为 int -> a 是 int
b = first(["x", "y"]) # T 被推断为 str -> b 是 str
# 因为 b 被认为是 str,下面这行是合法的:
print(b.upper()) # str 有 .upper()
# 而下面这行,mypy 会报错:int 没有 .upper()
print(a.upper()) # ← 故意的错误,给 mypy 抓
使用mypy检查结果:
toy_framework/gen1_typevar.py:27: error: "int" has no attribute "upper" [attr-defined]
Found 1 error in 1 file (checked 1 source file)
泛型第二块:Generic —— 自定义泛型类
========================================
class Box(Generic[T]) 让 Box 能被参数化成 Box[int]、Box[str]。
两个类型参数的版本 Processor[In, Out] 就是 langchain Runnable[Input, Output] 的骨架。
from typing import TypeVar, Generic
T = TypeVar("T")
class Box(Generic[T]):
"""一个装 T 类型东西的盒子。Generic[T] 让它可以写成 Box[int] 等。"""
def __init__(self, content: T):
self.content = content
def get(self) -> T: # 取出来的东西也是 T
return self.content
int_box: Box[int] = Box(123) # 声明这是"装 int 的盒子"
n = int_box.get() # mypy 知道 n 是 int
print(n + 1) # 合法:int 能加
# print(n.upper()) # 如果解开,mypy 会报错:int 没有 upper
# ---- 两个类型参数:这就是 Runnable[Input, Output] 的结构 ----
In = TypeVar("In")
Out = TypeVar("Out")
class Processor(Generic[In, Out]):
"""把 In 类型的输入,处理成 Out 类型的输出。像 langchain 的 Runnable。"""
def __init__(self, func):
self.func = func
def run(self, x: In) -> Out:
return self.func(x)
# 一个"把字符串转成长度(int)"的处理器:输入 str,输出 int
length: Processor[str, int] = Processor(len)
result = length.run("hello") # mypy 知道 result 是 int
print(result * 2) # 合法:int
# 故意用错:给 str->int 的处理器喂一个 int
bad = length.run(123) # ← mypy 报错:期望 str,给了 int
mypy检查结果:
toy_framework/gen2_generic.py:47: error: Argument 1 to "run" of "Processor" has incompatible type "int"; expected "str" [arg-type]
Found 1 error in 1 file (checked 1 source file)
当泛型类被实例化时,如果没有任何初始化参数,那么mypy无法做类型推断。
from typing import Generic, TypeVar, reveal_type
T = TypeVar("T")
# ---- 情况A:类型参数出现在构造函数实参里 -> mypy 能自动推断,不需注解 ----
class Box(Generic[T]):
def __init__(self, content: T):
self.content = content
def get(self) -> T:
return self.content
box = Box(123) # 没写注解
reveal_type(box) # mypy 仍推断为 Box[int]
box.get() + "x" # ← 报错:int + str,证明 mypy 知道它是 int
# ---- 情况B:构造函数【没有】用到类型参数 -> mypy 推不出,需要注解 ----
class Empty(Generic[T]):
def __init__(self) -> None: # 没有 T 类型的参数
self.items: list[T] = []
def add(self, x: T) -> None:
self.items.append(x)
e1 = Empty() # 没线索 -> mypy 只能当 Empty[Unknown]
reveal_type(e1)
e1.add(1)
e1.add("x") # 这里 mypy 可能不报错(因为 T 没被钉死)
e2: Empty[int] = Empty() # 显式注解,把 T 钉成 int
e2.add(1)
e2.add("x") # ← 现在报错:期望 int
mypy检查结果:
test4.py:12: note: Revealed type is "test4.Box[int]"
test4.py:13: error: Unsupported operand types for + ("int" and "str") [operator]
test4.py:23: error: Need type annotation for "e1" [var-annotated]
test4.py:24: note: Revealed type is "test4.Empty[Any]"
test4.py:30: error: Argument 1 to "add" of "Empty" has incompatible type "str"; expected "int" [arg-type]
Found 3 errors in 1 file (checked 1 source file)
泛型第三块:Protocol —— 结构化类型(鸭子类型 + 静态检查)
========================================
Protocol 表达"只要有某个方法/属性就行",不要求继承。
"长得像鸭子(有 quack 方法)就当鸭子",但由类型检查器静态验证。
from typing import Protocol
class SupportsInvoke(Protocol):
"""约定:任何有 invoke(str) -> str 方法的对象,都算 SupportsInvoke。"""
def invoke(self, x: str) -> str: ...
def other(self, y: int) -> str: ...
# 注意:下面两个类【都没有继承】SupportsInvoke,
# 但只要它们"长得像"(有匹配的 invoke 方法),就被认为符合。
class Upper:
def invoke(self, x: str) -> str:
return x.upper()
def other(self, z: int) -> str:
return 'nothing'
class Repeat:
def invoke(self, x: str) -> str:
return x * 2
def run_it(thing: SupportsInvoke, text: str) -> str:
# 这个函数只要求参数"有 invoke 方法",不管它具体是什么类
return thing.invoke(text)
print(run_it(Upper(), "hi")) # 合法:Upper 有匹配的 invoke和other
print(run_it(Repeat(), "ab")) # 不合法:Repeat 有invoke,但没有other
# 一个【没有 invoke 方法】的类:
class NoInvoke:
def other(self) -> str:
return "nope"
print(run_it(NoInvoke(), "x")) # ← mypy 报错:NoInvoke 不符合 SupportsInvoke,虽然有other,但是参数类型不符
mypy检查结果:
toy_framework/gen3_protocol.py:38: error: Argument 1 to "run_it" has incompatible type "Repeat"; expected "SupportsInvoke" [arg-type]
toy_framework/gen3_protocol.py:38: note: "Repeat" is missing following "SupportsInvoke" protocol member:
toy_framework/gen3_protocol.py:38: note: other
toy_framework/gen3_protocol.py:47: error: Argument 1 to "run_it" has incompatible type "NoInvoke"; expected "SupportsInvoke" [arg-type]
toy_framework/gen3_protocol.py:47: note: "NoInvoke" is missing following "SupportsInvoke" protocol member:
toy_framework/gen3_protocol.py:47: note: invoke
toy_framework/gen3_protocol.py:47: note: Following member(s) of "NoInvoke" have conflicts:
toy_framework/gen3_protocol.py:47: note: Expected:
toy_framework/gen3_protocol.py:47: note: def other(self, y: int) -> str
toy_framework/gen3_protocol.py:47: note: Got:
toy_framework/gen3_protocol.py:47: note: def other(self) -> str
Found 2 errors in 1 file (checked 1 source file)
泛型输出参数(由函数返回)一定得能够通过输入参数推导得出
随便填写的泛型参数会被mypy检查并标记为错误。
比如下边定义的泛型类:
from typing import TypeVar, Generic
T1 = TypeVar("T1")
T2 = TypeVar("T2")
class Foo(Generic[T1, T2]):
def __init__(self, x: T1):
self.x = x
def get_len(self) -> T2:
return len(self.x)
foo: Foo[str, int] = Foo('abc')
print(foo.get_len())
如果直接执行并不会报错,运行时本身就会忽略掉类型提示
但mypy检查结果如下:
test5.py:11: error: Incompatible return value type (got "int", expected "T2") [return-value]
test5.py:11: error: Argument 1 to "len" has incompatible type "T1"; expected "Sized" [arg-type]
Found 2 errors in 1 file (checked 1 source file)
结果说明,因为类中get_len函数会永远返回一个int类型的值,将来调用时输入的T2的实际类型不一定会是int,也就是调用方选择的类型有可能不成立。
第二个错误是说,self.x 被传入的是T1类型,当调用方选择一个不能使用len函数求长度的类型时就又不成立了。
正确的改法如下:
- 确定的返回类型不要使用泛型参数做类型提示。
- 输入给len函数的类型参数应该是能求长度的类型,用bound=Sized做限定。
from typing import TypeVar, Generic
from collections.abc import Sized
# T 限定为"有长度的东西"(有 __len__),这样才能对它 len()
T = TypeVar("T", bound=Sized)
class Foo(Generic[T]):
def __init__(self, x: T):
self.x = x
def get_len(self) -> int: # len 永远返回 int,就老实写 int
return len(self.x)
foo: Foo[str] = Foo("abc")
print(foo.get_len()) # 3
foo2: Foo[list[int]] = Foo([1, 2, 3]) # list 也是 Sized
print(foo2.get_len()) # 3
# foo3: Foo[int] = Foo(5) # 如果解开:mypy 报错,int 不是 Sized
另外,从python3.12开始,不需要再用TypeVar定义类型参数了,可以直接这样写:
from collections.abc import Sized
class Foo[T: Sized]: # 新语法的上界写法:[T: Sized]
def __init__(self, x: T):
self.x = x
def get_len(self) -> int:
return len(self.x)
foo: Foo[str] = Foo("abc")
print(foo.get_len())
定义好泛型类后在使用中如何指定类型变量
from typing import TypeVar
Y = TypeVar("Y")
class Foo[T]:
def __init__(self, content: T):
self.content = content
def get(self) -> T :
return self.content
foo = Foo[int](123)
print(foo.get())
foo2 = Foo[Y](123) # error: Type variable "test3.Y" is unbound [valid-type]
print(foo2.get())
foo3 = Foo[str](123) # error: Argument 1 to "Foo" has incompatible type "int"; expected "str" [arg-type]
print(foo3.get())
mypy检查结果:
test3.py:16: error: Type variable "test3.Y" is unbound [valid-type]
test3.py:16: note: (Hint: Use "Generic[Y]" or "Protocol[Y]" base class to bind "Y" inside a class)
test3.py:16: note: (Hint: Use "Y" in function signature to bind "Y" inside a function)
test3.py:19: error: Argument 1 to "Foo" has incompatible type "int"; expected "str" [arg-type]
Found 2 errors in 1 file (checked 1 source file)
类型变量的作用域应该在泛型类或泛型函数中
class Foo[T]:
def __init__(self, content: T):
self.content = content
def get(self) -> T:
return self.content
# 正确:在一个【泛型函数】内部,U 是当前作用域里存在的类型变量,
# 把 U 透传给 Foo[U] 就合法。
def wrap[U](x: U) -> Foo[U]:
return Foo(x) # 返回 Foo[U],U 由调用者传入的 x 决定
a = wrap(123) # U=int -> Foo[int]
b = wrap("hello") # U=str -> Foo[str]
reveal_type(a)
reveal_type(b)
# 正确:在一个【泛型类】内部,K 在类作用域里存在,可传给 Foo[K]
class Box[K]:
def __init__(self, item: K):
self.item = item
def to_foo(self) -> Foo[K]: # K 是 Box 的类型参数,在此可用
return Foo(self.item)
c = Box(3.14).to_foo()
reveal_type(c) # Foo[float]
mypy输出:
(PythonStudy) roland@RolanddeMacBook-Air PythonStudy % uv run -m mypy toy_framework/foo_passthrough.py
toy_framework/foo_passthrough.py:15: note: Revealed type is "foo_passthrough.Foo[int]"
toy_framework/foo_passthrough.py:16: note: Revealed type is "foo_passthrough.Foo[str]"
toy_framework/foo_passthrough.py:27: note: Revealed type is "foo_passthrough.Foo[float]"
Success: no issues found in 1 source file
三种"使用泛型类"的方式,分清作用域
| 写法 | 例子 | 前提 | 结果 |
|---|---|---|---|
| 代入具体类型 | Foo[int](123) |
随时可用 | 具体的 Foo[int] |
| 代入类型变量 | Foo[U](在 def wrap[U] 内) |
U 必须在当前作用域里存在 |
仍是泛型,透传 U |
| 不标,靠推断 | Foo(123) |
随时可用 | mypy 推断成 Foo[int] |
泛型类的继承
一张表覆盖所有情况
核心思想:写泛型子类时,永远只回答【两个互相独立的问题】
轴1(必答):基类的类型参数 T,我要怎么处理?
(a) 钉死成具体类型 -> Base[int]
(b) 透传我自己的参数 -> Base[X]
(c) 留空(=Base[Any]) -> Base ← 一般不推荐
轴2(选答):我自己要不要声明新的类型参数?
(0) 不要 -> class Sub(...)
(1) 要一个 / 多个 -> class SubX / class SubK, V
把这两条轴交叉组合,就得到下面 6 个类。每个类顶部都标了它在
【轴1 / 轴2】上的取值,读的时候对号入座即可。
场景设定:Base = 一个"容器",能存/取 T 类型的东西。
from __future__ import annotations
from typing import Any, Protocol, reveal_type
# ================================================================
# 基类:一个装 T 的容器
# ================================================================
class Container[T]:
def __init__(self, item: T) -> None:
self._item = item
def get(self) -> T: # 取出来的就是 T
return self._item
def put(self, item: T) -> None: # 放进去的也必须是 T
self._item = item
# ================================================================
# 情况 1 —— 轴1:(a)钉死 int 轴2:(0)无自己的参数
# 最简单:子类完全具体化,自己不再是泛型。
# 适用:你就是想要一个"专门装 int 的容器"。
# ================================================================
class IntContainer(Container[int]):
def increment(self) -> None: # 可以安全地用 int 特有操作
self.put(self.get() + 1)
c1 = IntContainer(10)
reveal_type(c1.get()) # int
# IntContainer("hi") # ❌ 若解开:期望 int,给了 str
# ================================================================
# 情况 2 —— 轴1:(b)透传 轴2:(1)有一个参数,且就用它透传
# 最常见、最标准的写法:子类仍是泛型,基类的 T 完全由子类的 T 决定。
# 适用:你在给基类"加功能"但不想固定元素类型(90% 的自定义泛型子类)。
# ================================================================
class LoggingContainer[T](Container[T]):
"""在 put 时多打一行日志,其余行为不变。元素类型仍然自由。"""
def put(self, item: T) -> None:
print(f"[log] 存入: {item!r}")
super().put(item)
c2: LoggingContainer[str] = LoggingContainer("hello")
reveal_type(c2.get()) # str ← 透传:我给 str,基类的 T 就是 str
# ================================================================
# 情况 3 —— 轴1:(a)钉死 str 轴2:(1)有一个【独立】参数
# 关键点:子类的类型参数 Meta 和 基类的 T 完全无关。
# 基类那格永远装 str;Meta 用在子类自己新增的字段上。
# 适用:基类的槽位语义固定(比如永远存"名字"),但你想附带任意元数据。
# ================================================================
class NamedContainer[Meta](Container[str]):
"""基类存一个 str 名字;Meta 是额外附带的元数据,类型独立。"""
def __init__(self, name: str, meta: Meta) -> None:
super().__init__(name) # 传给基类的必须是 str
self.meta = meta
def get_meta(self) -> Meta:
return self.meta
c3 = NamedContainer("订单A", meta={"priority": 1})
reveal_type(c3.get()) # str ← 来自基类(钉死)
reveal_type(c3.get_meta()) # dict[str, int] ← 来自独立参数 Meta
# ================================================================
# 情况 4 —— 轴1:(b)透传其中一个 轴2:(1)有【两个】参数
# 子类有 K、V 两个参数:V 透传给基类,K 是子类自己的。
# 演示"部分透传":并非所有自己的参数都要喂给基类。
# 适用:像一个带 key 的容器,value 存进基类,key 自己管。
# ================================================================
class KeyedContainer[K, V](Container[V]):
"""基类存 value(类型 V,透传);key(类型 K)是子类独有。"""
def __init__(self, key: K, value: V) -> None:
super().__init__(value) # value 透传给基类 Container[V]
self.key = key
def get_key(self) -> K:
return self.key
c4: KeyedContainer[str, int] = KeyedContainer("age", 30)
reveal_type(c4.get()) # int ← V 透传给基类
reveal_type(c4.get_key()) # str ← K 独立
# ================================================================
# 情况 5 —— 轴1:(c)留空 轴2:(0)无
# 写 Container(不带方括号)= Container[Any]。
# 基类的 T 退化成 Any,mypy 对它【彻底放弃检查】—— 危险!
# 适用:几乎不推荐。仅在你确实不关心类型时,且要清楚这等于关掉检查。
# ================================================================
class AnyContainer(Container): # 等价于 Container[Any]
pass
c5 = AnyContainer(123)
reveal_type(c5.get()) # Any ← 注意:不是 int,是 Any
# c5.get().nonexistent_method() # ← mypy【不报错】(Any 吞掉一切),但运行时会 AttributeError 崩掉!
# 这正是留空的危险:静态检查被彻底关掉了。
# ================================================================
# 情况 6 —— 轴1:(b)透传但【加约束】 轴2:(1)有一个带 bound 的参数
# 子类可以在透传的同时,给自己的类型参数加上限(bound),
# 从而在子类内部安全地使用该上限类型的方法。
# 适用:你想让容器只装"可比较大小"的东西,并提供 max() 之类方法。
# ================================================================
class Comparable(Protocol): # 结构化协议:有 __lt__ 就算
def __lt__(self, other: Any) -> bool: ...
class SortedContainer[T: Comparable](Container[T]):
"""T 被约束为 Comparable(可比较),于是内部能安全地做大小比较。"""
def is_greater_than(self, other: T) -> bool:
return other < self.get() # 合法:T 保证有 __lt__
c6: SortedContainer[int] = SortedContainer(5)
reveal_type(c6.get()) # int
print(c6.is_greater_than(3)) # True
# SortedContainer(object()) # ❌ 若解开:object 不满足 Comparable
# ================================================================
# 小结表(把上面 6 个类横向对照)
# ----------------------------------------------------------------
# 类 | 轴1 基类T怎么办 | 轴2 自己的参数 | get() 返回
# IntContainer | 钉死 int | 无 | int
# LoggingContainer[T] | 透传 T | 一个(透传) | 跟着 T
# NamedContainer[Meta] | 钉死 str | 一个(独立) | str
# KeyedContainer[K, V] | 透传 V | 两个(V透/K独) | 跟着 V
# AnyContainer | 留空(=Any) | 无 | Any(危险)
# SortedContainer[T:..] | 透传 T(带约束) | 一个(有bound)| 跟着 T
# ================================================================
if __name__ == "__main__":
c1.increment()
print("IntContainer:", c1.get()) # 11
c2.put("world")
print("KeyedContainer:", c4.get_key(), "->", c4.get())
print("SortedContainer 5 > 3 ?", c6.is_greater_than(3))
mypy检查结果:
toy_framework/inherit_all.py:48: note: Revealed type is "int"
toy_framework/inherit_all.py:64: note: Revealed type is "str"
toy_framework/inherit_all.py:83: note: Revealed type is "str"
toy_framework/inherit_all.py:84: note: Revealed type is "dict[str, int]"
toy_framework/inherit_all.py:103: note: Revealed type is "int"
toy_framework/inherit_all.py:104: note: Revealed type is "str"
toy_framework/inherit_all.py:117: note: Revealed type is "Any"
toy_framework/inherit_all.py:137: note: Revealed type is "int"
Success: no issues found in 1 source file
这里面的场景5要注意。 如果我们用基类来直接实例化一个对象,比如Container(123),那么mypy是可以推断出类型参数此是为int。但是继承它的子类在实例化AnyContainer(123)
时却推断类型为Any,这是为什么呢? 原因是,子类在定义时没有传给基类任何类型,相当于继承自一个未使用类型提示的父类,当然什么类型都可以传,也就等同于是Any类型。
除非它自己也声明了类型参数,并且透传给基类,这时再实例化时因为调用了基类的__init__函数,mypy就可以通过传入的实际值来推断透传的类型参数到底是什么类型。
这个例子的设计思路
把它组织成一张表 = 两条独立的轴交叉。场景统一是"一个装 T 的容器 Container[T]",6 个子类各占据表格的一格:
| 类 | 轴1:基类T怎么办 | 轴2:自己的参数 | get() 返回 |
|---|---|---|---|
IntContainer |
钉死 int |
无 | int |
LoggingContainer[T] |
透传 T |
一个(就用来透传) | 跟着 T |
NamedContainer[Meta] |
钉死 str |
一个(独立,不喂给基类) | str |
KeyedContainer[K, V] |
透传 V |
两个(V 透传、K 独立) |
跟着 V |
AnyContainer |
留空(= Any) |
无 | Any(危险) |
SortedContainer[T: Comparable] |
透传 + 加约束 | 一个(带 bound) | 跟着 T |
框架里经常看到的两个导入
from __future__ import annotations
它不是普通的 import,而是一个编译指令(叫 "future statement",PEP 563)。所以你看不到 annotations 这个名字在后面"被使用"——它根本不是一个模块对象,而是给编译器的一句话:"这个文件里,把所有类型注解都当字符串处理,不要在运行时求值。"
具体三点:
- 注解被"延迟求值"(变成字符串)。没有这行时,def f(x: int) 里的 int 会在函数定义时真的被执行一次,存成 <class 'int'>。有了这行,int 只是原样存成字符串 'int',压根不执行。
- 由此带来几个实际好处(也是库里到处用它的原因):
- 前向引用不用加引号:上面 make() -> Widget 里 Widget 还没定义、以及类在自己方法里返回自己(clone(self) -> Widget),平时得写成 -> "Widget",有了 future 就能直接写。
- 避免循环导入:注解里引用的类型不再在运行时求值,所以哪怕那个类型来自一个会导致循环导入的模块,只要不真的在运行时用它,就不会触发导入。
- 省一点启动开销:大量注解不再逐个求值。
- 能在旧版 Python 上用新语法:比如 X | Y、list[int] 这些在老版本运行时会报错,但当字符串就不会。
- 代价:那些运行时要读注解的框架(pydantic、fastapi、dataclasses)现在拿到的是字符串,得自己用 typing.get_type_hints() 去把 'int' 解析回真正的 int。而解析时那个名字必须在作用域里能找到——找不到就报 NameError。这也是为什么有时加了这行反而让某些框架出问题。
▎ 简单记:这行让"类型注解"从"运行时真会执行的代码"退化成"纯字符串标签"。 对纯给 mypy/IDE 看的注解毫无损失,还顺带解决前向引用和循环导入。我代码里习惯加它,就是图前向引用方便。
from typing import TYPE_CHECKING
TYPE_CHECKING 是 typing 里的一个常量,它有个"双面"特性:
- 运行时它永远是 False;
- 但 mypy / IDE 在分析时,把它当成 True。
于是这个模式就出现了:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from heavy_module import BigThing # 只有类型检查器会"看到"这行;运行时根本不执行
def process(x: "BigThing") -> None: # 注解里用得上它(注意要么加引号,要么配 future)
...
它解决什么问题? 有些类型你只在注解里需要,运行时代码根本不用它的实体。但普通 import 会真的执行,带来两个麻烦:
- 循环导入:A 模块的注解要引用 B 的类型,B 又要引用 A。放进 if TYPE_CHECKING: 就打破死锁——运行时谁都不导入谁,只有类型检查器两边都看得到。
- 昂贵/重的导入:某个类型来自一个加载很慢的大模块,而你只是拿它当注解。塞进 TYPE_CHECKING 就能避免为了一个类型提示而在运行时拖慢启动。
总结:
两者经常搭配使用:if TYPE_CHECKING 里导入的名字,运行时并不存在,所以引用它的注解必须不能在运行时求值——要么给注解加引号,要么在文件顶部加 from future import annotations。这就是为什么你常在库里同时看到这两行:future 让注解变字符串(运行时不求值),TYPE_CHECKING 让那些"只为注解服务"的重量级导入在运行时消失。
一句话总结二者的共同主题:它们都是在把"类型信息"和"运行时真正执行的代码"解耦——注解给静态检查器看就够了,能不在运行时付出代价就不付出。你现在读框架源码时,看到这两行就知道:"哦,这里在刻意让类型注解不在运行时生效。"
要不要进 pydantic?它恰好是反过来的一派——故意在运行时读注解、做真实校验,和这两个"能躲就躲"的机制正好形成鲜明对照,接着讲会很连贯。
旧写法的泛型类的继承
当子类完全复用了基类的泛型参数时,可以直接这样写: class Sub(Base[T1, T2]), 它相当用新语法子类将两个自己的泛型参数透传给基类: class SubX, Y
如果子类不光要继承基类的泛型参数,还有它自己需要用的泛型参数,那么要让子类不光继承基类,还要继承Generic(定义所有类型参数)
from typing import TypeVar, Generic, reveal_type
T = TypeVar("T")
Y = TypeVar("Y")
T1 = TypeVar("T1")
T2 = TypeVar("T2")
T3 = TypeVar("T3")
class Base(Generic[T, Y]): # 新语法的 class Base[T, Y]
def __init__(self, a: T, b: Y) -> None:
self.a = a
self.b = b
# ---- 情况1:纯透传,换个变量名。老语法【无需】写 Generic ----
# 等价新语法: class Sub[T1, T2](Base[T1, T2])
class Sub(Base[T1, T2]):
pass
s = Sub(1, "x")
reveal_type(s) # 期望 Sub[int, str]
# ---- 情况2:子类多一个自己的参数 T1(不传给基类)----
# 新语法一行搞定: class Sub2[T1, T2, T3](Base[T2, T3])
# 老语法:必须显式补 Generic[T1, T2, T3],把【全部参数+顺序】写出来
class Sub2(Base[T2, T3], Generic[T1, T2, T3]):
def __init__(self, extra: T1, a: T2, b: T3) -> None:
super().__init__(a, b)
self.extra = extra # T1 是子类独有的字段
def get_extra(self) -> T1:
return self.extra
s2 = Sub2(3.14, 1, "x") # extra=float, a=int, b=str
reveal_type(s2) # 期望 Sub2[float, int, str]
reveal_type(s2.get_extra()) # 期望 float
reveal_type(s2.a) # 期望 int (来自基类 T2)
reveal_type(s2.b) # 期望 str (来自基类 T3)
# ---- 反例:如果【忘了】写 Generic,只写 Base[T2, T3] 会怎样 ----
# 那么 T1 根本没被声明为类参数,子类只会是 Wrong[T2, T3],T1 变成"游离"的
class Wrong(Base[T2, T3]):
def __init__(self, extra: T1, a: T2, b: T3) -> None:
super().__init__(a, b)
self.extra = extra
def get_extra(self) -> T1: # 这里的 T1 没有绑到类,行为会退化
return self.extra
w = Wrong(3.14, 1, "x")
reveal_type(w) # 看它到底带几个参数
mypy输出结果:
(PythonStudy) roland@RolanddeMacBook-Air PythonStudy % uv run -m mypy test4.py
test4.py:22: note: Revealed type is "test4.Sub[int, str]"
test4.py:36: note: Revealed type is "test4.Sub2[float, int, str]"
test4.py:37: note: Revealed type is "float"
test4.py:38: note: Revealed type is "int"
test4.py:39: note: Revealed type is "str"
test4.py:48: error: A function returning TypeVar should receive at least one argument containing the same TypeVar [type-var]
test4.py:49: error: Incompatible return value type (got "T1@__init__", expected "T1@get_extra") [return-value]
test4.py:52: note: Revealed type is "test4.Wrong[int, str]"
Found 2 errors in 1 file (checked 1 source file)
结果显示出了一个问题,就是子类的__init__函数里多了一个没有在类中定义的类型参数T1,那么这个类型参数T1就成了函数自己的类型参数。
而get_extra函数的返回值使用了T1,但因为T1是__init__函数自己的类型参数而不是属于类的,所以会报错。
泛型参数的顺序是重要的
from typing import TypeVar, Generic, reveal_type
T2 = TypeVar("T2")
T3 = TypeVar("T3")
T1 = TypeVar("T1")
class Base(Generic[T2, T3]):
def __init__(self, a: T2, b: T3) -> None:
self.a = a
self.b = b
# 两个子类,内容完全一样,只有 Generic[...] 里的【顺序】不同
class OrderA(Base[T2, T3], Generic[T1, T2, T3]): # 顺序: T1, T2, T3
def __init__(self, extra: T1, a: T2, b: T3) -> None:
super().__init__(a, b); self.extra = extra
class OrderB(Base[T2, T3], Generic[T2, T3, T1]): # 顺序: T2, T3, T1
def __init__(self, extra: T1, a: T2, b: T3) -> None:
super().__init__(a, b); self.extra = extra
# 用【显式下标】声明变量,看谁对应哪个槽
xa: OrderA[float, int, str] = OrderA(3.14, 1, "x") # 期望: extra=float, a=int, b=str
reveal_type(xa.extra) # 第1个槽
reveal_type(xa.a) # 基类第1参数
reveal_type(xa.b) # 基类第2参数
xb: OrderB[int, str, float] = OrderB(3.14, 1, "x") # 顺序变了: a=int, b=str, extra=float
reveal_type(xb.extra)
reveal_type(xb.a)
reveal_type(xb.b)
# 证明顺序真的绑死了下标位置:按 OrderA 的顺序去填 OrderB 就会类型错
bad: OrderB[float, int, str] = OrderB(3.14, 1, "x") # ← 期望报错
mypy检查结果:
(PythonStudy) roland@RolanddeMacBook-Air PythonStudy % uv run -m mypy test6.py
test6.py:26: note: Revealed type is "float"
test6.py:27: note: Revealed type is "int"
test6.py:28: note: Revealed type is "str"
test6.py:31: note: Revealed type is "float"
test6.py:32: note: Revealed type is "int"
test6.py:33: note: Revealed type is "str"
test6.py:36: error: Argument 1 to "OrderB" has incompatible type "float"; expected "str" [arg-type]
test6.py:36: error: Argument 3 to "OrderB" has incompatible type "str"; expected "int" [arg-type]
Found 2 errors in 1 file (checked 1 source file)
实例化类的时候,实参是按照顺序传递给__init__函数的,而__init__函数的类型提示里标明了每个位置参数映射到哪个类型参数上。
Generic里比基类多出来的类型参数就是属于子类自己的类型参数。
使用新的泛型语法模拟实现langchain Runnable类
# 模拟实现langchain Runnable类和管道操作
from __future__ import annotations
from typing import Callable
from typing import reveal_type
from abc import ABC, abstractmethod
class Runnable[Input, Output](ABC):
@abstractmethod
def invoke(self, input: Input) -> Output: ...
def __or__[Other](self, other: Runnable[Output, Other]) -> Runnable[Input, Other]:
return RunnableSequence(self, other)
class RunnableSequence[Input, Output](Runnable[Input, Output]):
def __init__[Other](self, first: Runnable[Input, Other], second: Runnable[Other, Output]):
self.first = first
self.second = second
def invoke(self, input: Input) -> Output:
middle = self.first.invoke(input)
return self.second.invoke(middle)
class RunnableLambda[Input, Output](Runnable[Input, Output]):
def __init__(self, func: Callable[[Input], Output]):
self.func = func
def invoke(self, input: Input) -> Output:
return self.func(input)
def prompt(input: str) -> str:
return f"请讲一个关于 \"{input}\" 的笑话"
def model(input: str) -> str:
return f"我是 llm, 这是对你的要求: {input} 的回应!"
def parse(input: str) -> int:
return len(input)
def boo(input: int) -> bool:
return bool(input)
# 当函数类型提示完整,且由RunnableLambda的泛型参数透传时,可以直接由函数类型推断,不用显示对变量或构造器加类型提示
# prompt_runnable = RunnableLambda[str, str](prompt)
# model_runnable: RunnableLambda[str, str] = RunnableLambda(model)
# parse_runnable: Runnable[str, int] = RunnableLambda(parse)
# boo_runnable: Runnable[int, bool] = RunnableLambda(boo)
# 直接使用函数的类型注解推断
prompt_runnable = RunnableLambda(prompt)
model_runnable = RunnableLambda(model)
parse_runnable = RunnableLambda(parse)
boo_runnable = RunnableLambda(boo)
run_seq1 = prompt_runnable | model_runnable
reveal_type(run_seq1)
run_seq2 = prompt_runnable | model_runnable | parse_runnable
reveal_type(run_seq2)
run_seq3 = prompt_runnable | model_runnable | parse_runnable | boo_runnable
reveal_type(run_seq3)
run_seq4 = prompt_runnable | boo_runnable | model_runnable # 这里故意写个错误的拼接类型
print(run_seq1.invoke('公鸡'))
print(run_seq2.invoke('公鸡'))
print(run_seq3.invoke('公鸡'))
重载(@overload)和类型提示的关系
Python 根本没有"真正的"运行时函数重载(不像 Java/C++ 那样按参数类型自动分派)。如果你写两个同名 def f,第二个只会覆盖第一个,不会共存。
@overload 是纯类型提示层面的东西:它让你给同一个函数声明多个"类型签名",好让类型检查器根据实参类型推出正确的返回类型。但运行时只有一个真正的实现。看真实例子:
from typing import overload
from typing import reveal_type
@overload
def parse(x: int) -> str: ... # 签名1:int 进 -> str 出
@overload
def parse(x: str) -> int: ... # 签名2:str 进 -> int 出
def parse(x): # ← 唯一真正的实现(没有类型标注也行)
if isinstance(x, int):
return str(x)
return len(x)
a = parse(123) # 检查器知道 a 是 str
b = parse("hello") # 检查器知道 b 是 int
reveal_type(a)
reveal_type(b)
mypy检查结果:
(PythonStudy) roland@RolanddeMacBook-Air PythonStudy % uv run -m mypy test.py
test.py:16: note: Revealed type is "str"
test.py:17: note: Revealed type is "int"
Success: no issues found in 1 source file
协变和逆变(covariant/contravariant)
旧语法示例:
型变(variance)演示:输出协变,输入逆变
只在"子类型能否替换"时才有可观察效果。用 Animal/Cat 演示。
口诀:输出(生产)协变 covariant,输入(消费)逆变 contravariant。
from typing import Protocol, TypeVar
class Animal: ...
class Cat(Animal): ... # Cat 是 Animal 的子类
# ========== 输出侧:协变(covariant) ==========
T_co = TypeVar("T_co", covariant=True)
class Producer(Protocol[T_co]):
def get(self) -> T_co: ... # 只出现在返回值(输出位置)
class CatFactory:
def get(self) -> Cat: return Cat() # 生产 Cat
class AnimalFactory:
def get(self) -> Animal: return Animal() # 生产 Animal
def wants_animal_producer(p: Producer[Animal]) -> None: ...
wants_animal_producer(CatFactory()) # ✅ OK:协变。要"能产Animal的",给"能产Cat的"没问题
wants_animal_producer(AnimalFactory()) # ✅ OK
# 反方向:
def wants_cat_producer(p: Producer[Cat]) -> None: ...
wants_cat_producer(AnimalFactory()) # ❌ 错:只产Animal,不保证是Cat
# ========== 输入侧:逆变(contravariant) ==========
T_contra = TypeVar("T_contra", contravariant=True)
class Consumer(Protocol[T_contra]):
def put(self, x: T_contra) -> None: ... # 只出现在参数(输入位置)
class AnimalSink:
def put(self, x: Animal) -> None: ... # 能吃任何 Animal
class CatSink:
def put(self, x: Cat) -> None: ... # 只能吃 Cat
def wants_cat_consumer(c: Consumer[Cat]) -> None: ...
wants_cat_consumer(AnimalSink()) # ✅ OK:逆变。要"能吃Cat的",给"能吃Animal的"更没问题
wants_cat_consumer(CatSink()) # ✅ OK
# 反方向:
def wants_animal_consumer(c: Consumer[Animal]) -> None: ...
wants_animal_consumer(CatSink()) # ❌ 错:只能吃Cat,喂它别的Animal会崩
新语法示例:
- 新语法里不能写 covariant=True——方括号语法根本没有这个参数位。想指定型变?改变量的使用位置,而不是加标记。
- 自动推断更不容易出错。旧语法有个经典坑:你声明了 covariant=True,却不小心把这个变量用在了输入位置,mypy 会报"covariant type variable used in contravariant position"。新语法不存在这个矛盾——它是看你实际怎么用再定型变,天然自洽。
- 少数需要手动控制型变的场景,PEP 695 也留了后门:可以写 class Foo[T_co] 这种命名约定(纯提示,无强制),或在极特殊情况下仍退回旧的 TypeVar(..., covariant=True)。但 99% 的情况,让它自动推断就是最佳实践。
▎ 旧语法:你告诉检查器"这个参数是协变的"(covariant=True),并自证没用错位置。
▎ 新语法:你只写 [T],检查器看你把 T 用在输出还是输入位置,自己算出型变。口诀从"你要遵守"变成了"检查器帮你执行"。
"""新语法:型变【自动推断】,不写 covariant/contravariant。"""
from typing import Protocol
class Animal: ...
class Cat(Animal): ...
# 旧语法要写: T_co = TypeVar("T_co", covariant=True)
# 新语法:直接 [T],检查器看到 T 只出现在【输出位置】-> 自动判定为协变
class Producer[T](Protocol):
def get(self) -> T: ... # T 只在返回值(输出)
# 旧语法要写: T_contra = TypeVar("T_contra", contravariant=True)
# 新语法:直接 [T],检查器看到 T 只出现在【输入位置】-> 自动判定为逆变
class Consumer[T](Protocol):
def put(self, x: T) -> None: ... # T 只在参数(输入)
class CatFactory:
def get(self) -> Cat: return Cat()
class AnimalSink:
def put(self, x: Animal) -> None: ...
# ---- 协变验证:要"能产Animal的",给"能产Cat的"应 OK ----
def wants_animal_producer(p: Producer[Animal]) -> None: ...
wants_animal_producer(CatFactory()) # ✅ 若型变没生效,这里会报错
# ---- 逆变验证:要"能吃Cat的",给"能吃Animal的"应 OK ----
def wants_cat_consumer(c: Consumer[Cat]) -> None: ...
wants_cat_consumer(AnimalSink()) # ✅ 若型变没生效,这里会报错
# ---- 反向应报错,证明推断是【真的】协变/逆变,不是随便放行 ----
class AnimalFactory:
def get(self) -> Animal: return Animal()
class CatSink:
def put(self, x: Cat) -> None: ...
def wants_cat_producer(p: Producer[Cat]) -> None: ...
wants_cat_producer(AnimalFactory()) # ❌ 期望报错:只产Animal不保证是Cat
def wants_animal_consumer(c: Consumer[Animal]) -> None: ...
wants_animal_consumer(CatSink()) # ❌ 期望报错:只能吃Cat,喂别的Animal会崩

浙公网安备 33010602011771号