ty 泛型可调用对象全解析:PEP 484 传统语法下的类型推断与特化
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
本篇技术指南以 Ruff 仓库内置的类型检查器ty(Rust 实现)的 mdtest 规格文档为主体,系统讲解"传统(Legacy,即 PEP 484 风格)语法下泛型可调用对象(generic callable)"的完整语义:从泛型函数、泛型类与可调用实例的泛型上下文识别,到通过类型别名与函数返回值命名泛型Callable、重载回调参与泛型推断、ParamSpec 变参的渐进性处理,以及多个高阶边界场景。读完本文,你将掌握 ty 如何处理"调用一个泛型 callable"时的类型变量推断、约束求解、重载匹配顺序与特化保留规则,并了解如何通过仓库内的 mdtest 用例复现与验证这些行为。
本文主体内容来源于 crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md,并结合 ty_extensions 内部 API 与 mdtest 测试框架 的源码进行印证。
背景:ty 类型检查器与 mdtest 规格测试
ty 是 Ruff 仓库中的下一代 Python 类型检查器,用 Rust 编写,核心语义实现位于 crates/ty_python_semantic 与 crates/ty 等 crate。它的正确性由一个名为mdtest的测试体系保障:Markdown 文档中的 Python 代码块会被抽取为待检查的源文件,代码里的reveal_type(...)注释声明期望的类型,# error: [...]声明期望的诊断,而# revealed:声明期望的推断结果。运行时由 crates/ty_test/src/lib.rs 调用 crates/mdtest 解析文档结构,逐段驱动类型检查并与期望值比对。
这些 mdtest 文档按主题组织在 crates/ty_python_semantic/resources/mdtest 目录下,其中 generics/legacy 专门覆盖 PEP 484 传统泛型语法(TypeVar、ParamSpec、Generic等),与 pep695(PEP 695type/def f[T]语法)形成对照。本文讨论的 callables.md 是其中关于"可调用对象"泛型行为的完整规格。代码块按 ruff.toml 配置以.pyi扩展名处理(py = "pyi"),行长上限 130 列,并启用格式化的 preview 模式。
测试中大量使用了ty_extensions._internal里的测试辅助符号,其声明位于 crates/ty_vendored/ty_extensions/_internal.pyi:
generic_context(input: Any) -> GenericContext | None:返回一个类、函数、方法或类型别名的泛型上下文(即它绑定的类型变量集合);输入非泛型时返回None;into_regular_callable(value: Any) -> Any:把值强制转换为常规的Callable类型(对应类型层面的RegularCallableTypeOf);TypeOf、Unknown、static_assert、is_subtype_of等,分别用于获取表达式推断类型、表示"未知动态类型"、断言与子类型检验。
下文所有示例直接取自该规格文档,# revealed:后即为 ty 实际推断并验证的结果。
可调用对象可以是泛型的
"可调用"并不局限于函数。泛型函数是最直观的例子:函数签名中的TypeVar使它在每次调用时可以被特化,ty 通过generic_context能观察到它绑定的类型变量。
from typing import Callable, ParamSpec, TypeVar from ty_extensions._internal import generic_context P = ParamSpec("P") T = TypeVar("T") def identity(t: T) -> T: return t # revealed: ty_extensions._internal.GenericContext[T@identity] reveal_type(generic_context(identity)) # revealed: Literal[1] reveal_type(identity(1)) def identity2(c: Callable[P, T]) -> Callable[P, T]: return c # revealed: ty_extensions._internal.GenericContext[P@identity2, T@identity2] reveal_type(generic_context(identity2)) # revealed: T -> T reveal_type(identity2(identity)) class CallableInstance: def __call__(self, value: int, /) -> str: return str(value) # revealed: (value: int, /) -> str reveal_type(identity2(CallableInstance()))观察要点:
GenericContext[T@identity]表示"identity这个泛型对象绑定了类型变量T";@后缀是 ty 对类型变量所属对象的标识。identity(1)在调用点推断为Literal[1],说明类型变量按实参精确特化,而非退化为int。identity2同时绑定P(ParamSpec)与T两个变量。- 把
identity(泛型函数)与CallableInstance()(带__call__的可调用实例)传给identity2均合法:前者得到T -> T的泛型 callable,后者得到精确的(value: int, /) -> str。
泛型类同样适用——调用类即实例化它,而实例化时其类型参数应当参与推断:
from typing import Generic class C(Generic[T]): def __init__(self, t: T) -> None: ... # revealed: ty_extensions._internal.GenericContext[T@C] reveal_type(generic_context(C)) # revealed: C[int] reveal_type(C(1))这里generic_context(C)揭示类对象C的泛型上下文为T@C,而C(1)调用则得到C[int],说明构造函数调用同样完成了类型参数的特化。
强制转换为 Callable 时保留泛型性
ty 提供into_regular_callable把任意可调用值"规范化"为常规Callable(等价于类型层面的RegularCallableTypeOf)。关键语义是:转换后的 callable 不会丢失泛型信息——它仍然记得自己是泛型的,继续参与调用点的类型变量推断。
from ty_extensions._internal import into_regular_callable # revealed: T -> T reveal_type(into_regular_callable(identity)) # revealed: ty_extensions._internal.GenericContext[T@identity] reveal_type(generic_context(into_regular_callable(identity))) # revealed: Literal[1] reveal_type(into_regular_callable(identity)(1)) # revealed: **P, T -> T) -> ((**P) -> T) reveal_type(into_regular_callable(identity2)) # revealed: ty_extensions._internal.GenericContext[P@identity2, T@identity2] reveal_type(generic_context(into_regular_callable(identity2))) # revealed: T -> T reveal_type(into_regular_callable(identity2)(identity)) # revealed: T -> C[T] reveal_type(into_regular_callable(C)) # revealed: ty_extensions._internal.GenericContext[T@C] reveal_type(generic_context(into_regular_callable(C))) # revealed: C[int] reveal_type(into_regular_callable(C)(1))注意显示格式:T -> T中的方括号前缀即"泛型 callable 的类型变量列表",[**P, T]表明 ParamSpec 与 TypeVar 混合绑定。三个场景(泛型函数、带 ParamSpec 的泛型函数、泛型类)在转换后都能被generic_context再次识别,并能在调用时继续特化,说明规范化操作只改变"调用风格"(如去除描述符语义),不改变类型参数的开放性。
命名泛型 Callable:类型别名
当需要反复引用某个泛型Callable形状时,最直接的方式是类型别名。关键规则是:别名中引用 TypeVar 的Callable本身是泛型的,而使用该别名的函数不一定泛型。
from typing import Callable, TypeVar from ty_extensions._internal import generic_context T = TypeVar("T") IdentityCallable = Callable[[T], T] def decorator_factory() -> IdentityCallable[T]: def decorator(fn: T) -> T: return fn # revealed: ty_extensions._internal.GenericContext[T@decorator] reveal_type(generic_context(decorator)) # revealed: Literal[1] reveal_type(decorator(1)) return decorator # Note that `decorator_factory` returns a generic callable, but is not itself generic! # revealed: None reveal_type(generic_context(decorator_factory)) # revealed: T'return -> T'return reveal_type(decorator_factory()) # revealed: ty_extensions._internal.GenericContext[T'return@decorator_factory] reveal_type(generic_context(decorator_factory())) # revealed: Literal[1] reveal_type(decorator_factory()(1))这段代码揭示了两个精细语义:
- 内部函数
decorator是泛型的(GenericContext[T@decorator]),但它不是工厂函数本身——generic_context(decorator_factory)返回None,因为decorator_factory没有自己的类型参数。 decorator_factory()的返回值是一个新的泛型 callable,其类型变量被标注为T'return(ty 用'return后缀表示由返回类型创建的 fresh 类型变量),且decorator_factory()(1)仍然能正确推断出Literal[1]。
命名泛型 Callable 与 ParamSpec:类型别名
当 callable 形状中涉及 ParamSpec 时,同样的模式成立——类型别名可以同时携带P与T:
from typing import Callable, ParamSpec, TypeVar from ty_extensions._internal import generic_context P = ParamSpec("P") T = TypeVar("T") IdentityCallable = Callable[[Callable[P, T]], Callable[P, T]] def decorator_factory() -> IdentityCallable[P, T]: def decorator(fn: Callable[P, T]) -> Callable[P, T]: return fn # revealed: ty_extensions._internal.GenericContext[P@decorator, T@decorator] reveal_type(generic_context(decorator)) return decorator # Note that `decorator_factory` returns a generic callable, but is not itself generic! # revealed: None reveal_type(generic_context(decorator_factory)) def identity(t: T) -> T: return t # revealed: **P'return, T'return -> T'return, /) -> ((**P'return) -> T'return) reveal_type(decorator_factory()) # revealed: ty_extensions._internal.GenericContext[P'return@decorator_factory, T'return@decorator_factory] reveal_type(generic_context(decorator_factory())) # revealed: T -> T reveal_type(decorator_factory()(identity)) # revealed: Literal[1] reveal_type(decorator_factory()(identity)(1))[**P'return, T'return]说明返回的 callable 同时携带"返回值特化的 ParamSpec"与 TypeVar;把identity传入后,ParamSpec 被具体化为identity的签名,最终得到T -> T,再调用一次即得Literal[1]。整个链条说明 ParamSpec 特化是签名保真的,参数列表的形状被完整传递。
命名泛型 Callable:函数返回值
除了类型别名,也可以直接在返回注解中写出泛型Callable。ty 的判定规则是:如果某个 TypeVar 只出现在Callable内部,且只出现在返回类型位置,那么泛型的是那个被返回的 callable,而不是函数本身。
from typing import Callable, TypeVar from ty_extensions._internal import generic_context T = TypeVar("T") def decorator_factory() -> Callable[[T], T]: def decorator(fn: T) -> T: return fn # revealed: ty_extensions._internal.GenericContext[T@decorator] reveal_type(generic_context(decorator)) return decorator # Note that `decorator_factory` returns a generic callable, but is not itself generic! # revealed: None reveal_type(generic_context(decorator_factory)) # revealed: T'return -> T'return reveal_type(decorator_factory()) # revealed: ty_extensions._internal.GenericContext[T'return@decorator_factory] reveal_type(generic_context(decorator_factory())) # revealed: Literal[1] reveal_type(decorator_factory()(1))与别名版本结果完全一致:工厂不泛型(None),返回值是 fresh 类型变量T'return的泛型 callable。
反例:TypeVar 同时出现在参数中时
规则的另一半同样重要:如果该 TypeVar 还出现在某个参数中,那么泛型的是函数本身,返回的Callable不再泛型。此时返回的 callable 已被参数特化:
def outside_callable(t: T) -> Callable[[T], T]: raise NotImplementedError # revealed: ty_extensions._internal.GenericContext[T@outside_callable] reveal_type(generic_context(outside_callable)) # revealed: (int, /) -> int reveal_type(outside_callable(1)) # revealed: None reveal_type(generic_context(outside_callable(1))) # error: [invalid-argument-type] outside_callable(1)("string")outside_callable(1)返回(int, /) -> int——一个非泛型、参数已固化为int的 callable,因此generic_context为None;再向它传"string"必然触发invalid-argument-type诊断。这正是"泛型归属"判定的分水岭:TypeVar 出现位置决定谁持有泛型身份。
带 ParamSpec 的函数返回值
ParamSpec 版本遵循完全相同的规则:
from typing import Callable, ParamSpec, TypeVar from ty_extensions._internal import generic_context P = ParamSpec("P") T = TypeVar("T") def decorator_factory() -> Callable[[Callable[P, T]], Callable[P, T]]: def decorator(fn: Callable[P, T]) -> Callable[P, T]: return fn # revealed: ty_extensions._internal.GenericContext[P@decorator, T@decorator] reveal_type(generic_context(decorator)) return decorator # Note that `decorator_factory` returns a generic callable, but is not itself generic! # revealed: None reveal_type(generic_context(decorator_factory)) def identity(t: T) -> T: return t # revealed: **P'return, T'return -> T'return, /) -> ((**P'return) -> T'return) reveal_type(decorator_factory()) # revealed: ty_extensions._internal.GenericContext[P'return@decorator_factory, T'return@decorator_factory] reveal_type(generic_context(decorator_factory())) # revealed: T -> T reveal_type(decorator_factory()(identity)) # revealed: Literal[1] reveal_type(decorator_factory()(identity)(1))一个值得注意的实现细节是:传统语法工厂的 return 语句是按照其返回类型的词法形式(lexical form)来检查的,即使返回的 callable 接受并返回另一个 callable 也是如此:
from typing import NoReturn class WrappedCallable: def __call__(self, *args: object, **kwargs: object) -> NoReturn: raise NotImplementedError def nested_callable_factory() -> Callable[[Callable[P, T]], Callable[P, T]]: return lambda callback: WrappedCallable()这里lambda返回WrappedCallable(一个NoReturn可调用实例),ty 将其与Callable[[Callable[P, T]], Callable[P, T]]的词法形式比对,验证了"返回注解按字面形状约束 return 语句"的检查策略。
反例:ParamSpec 同时出现在参数中
同样地,ParamSpec 一旦出现在参数里,泛型身份就归属函数:
def outside_callable(func: Callable[P, T]) -> Callable[P, T]: raise NotImplementedError # revealed: ty_extensions._internal.GenericContext[P@outside_callable, T@outside_callable] reveal_type(generic_context(outside_callable)) def int_identity(x: int) -> int: return x # revealed: (x: int) -> int reveal_type(outside_callable(int_identity)) # revealed: None reveal_type(generic_context(outside_callable(int_identity))) # error: [invalid-argument-type] outside_callable(int_identity)("string")outside_callable绑定P@outside_callable与T@outside_callable;传入int_identity后返回(x: int) -> int,不再泛型,因此generic_context为None,后续错误调用触发诊断。
重载可调用对象作为泛型 Callable 参数
当一个重载(overload)函数被传给泛型Callable参数时,ty 要求整个重载集合作为一个整体与目标 callable 兼容。类型变量的推断只从第一个匹配的重载进行,而不是把各重载的参数类型取并集——后者会对逆变(contravariant)类型变量产生无法满足的期望类型。
from typing import Callable, TypeVar, overload T = TypeVar("T") def accepts_callable(converter: Callable[[T], None]) -> T: raise NotImplementedError @overload def f(val: str) -> None: ... @overload def f(val: bytes) -> None: ... def f(val: str | bytes) -> None: pass reveal_type(accepts_callable(f)) # revealed: str | bytesf的每个重载都是(str) -> None或(bytes) -> None,与Callable[[T], None]兼容;ty 取第一个匹配重载的参数类型str与bytes的并集,得到str | bytes——这正是"重载集合整体兼容 + 从首个匹配重载推断"两条规则的直接体现。
被拒绝的重载回调保留有效特化
重载回调中可能包含某个返回类型违反类型变量上界(bound)或声明约束(constraints)的备选分支。此时 ty 必须忽略无效分支、保留有效特化,且结果与重载声明顺序无关:
from typing import Callable, TypeVar, overload Bounded = TypeVar("Bounded", bound=int) Constrained = TypeVar("Constrained", int, bytes) @overload def invalid_first(value: str) -> str: ... @overload def invalid_first(value: int) -> int: ... def invalid_first(value: str | int) -> str | int: return value @overload def invalid_last(value: int) -> int: ... @overload def invalid_last(value: str) -> str: ... def invalid_last(value: str | int) -> str | int: return value def infer_bound(callback: Callable[..., Bounded]) -> Bounded: raise NotImplementedError def infer_constrained(callback: Callable[..., Constrained]) -> Constrained: raise NotImplementedError reveal_type(infer_bound(invalid_first)) # revealed: int reveal_type(infer_bound(invalid_last)) # revealed: int reveal_type(infer_constrained(invalid_first)) # revealed: int reveal_type(infer_constrained(invalid_last)) # revealed: intinfer_bound的返回类型Bounded有上界int,因此str分支的返回类型(str)违反上界,被排除;int分支成为有效特化。invalid_first与invalid_last只是把无效分支放在首位或末位,结果均为int——说明 ty 在求解约束时会扫描全部重载、剔除违反 bound/constraints 的备选,而不是贪心地采用第一个分支。Constrained(int, bytes)同理,str不在约束集合中,被排除后剩int。
约束 TypeVar 的重载:整体匹配 Callable[[T], T]
当T被其它参数约束为某个联合时,重载 callable 仍必须作为一个整体去满足Callable[[T], T]的形状:
from typing import Callable, TypeVar, overload T = TypeVar("T") def apply_twice(converter: Callable[[T], T], left: T, right: T) -> tuple[T, T]: return converter(left), converter(right) @overload def f(val: int) -> int: ... @overload def f(val: str) -> str: ... def f(val: int | str) -> int | str: return val x: int | str = 1 y: int | str = "a" result = apply_twice(f, x, y) # revealed: tuple[int | str, int | str] reveal_type(result)实参x、y都是int | str,它们把T约束为int | str;重载集合(int)->int与(str)->str整体上恰好与Callable[[int | str], int | str]的形状兼容(联合中的每个成员都能匹配某个重载),因此apply_twice返回tuple[int | str, int | str]。
泛型工厂返回重载可调用对象
泛型 callable 工厂返回的重载callable,仍应可赋值给声明的泛型 callable 返回类型:
from collections.abc import Callable, Coroutine from typing import Any, TypeVar, overload S = TypeVar("S") T = TypeVar("T") U = TypeVar("U") def singleton(flag: bool = False) -> Callable[[Callable[[int], S]], Callable[[int], S]]: @overload def wrapper(func: Callable[[int], Coroutine[Any, Any, T]]) -> Callable[[int], Coroutine[Any, Any, T]]: ... @overload def wrapper(func: Callable[[int], U]) -> Callable[[int], U]: ... def wrapper(func: Callable[[int], Coroutine[Any, Any, T] | U]) -> Callable[[int], Coroutine[Any, Any, T] | U]: return func return wrapperwrapper拥有两个重载:一个针对协程返回类型Coroutine[Any, Any, T](特化T),一个兜底特化U。整个重载集合须整体满足Callable[[Callable[[int], S]], Callable[[int], S]],即返回的wrapper无论走哪个分支,其"参数与返回值同为接受int的 callable"的形态都要成立。该用例没有显式reveal_type,它的意义在于回归锁定:工厂返回重载 callable 时不得产生赋值/匹配错误。
部分注解重载的返回类型推断
当重载集合中"字面量专属"分支没有注解返回类型、而兜底分支返回object时,从整个回调推断返回类型应保留object:
from typing import Callable, Literal, TypeVar, overload from typing_extensions import assert_type R = TypeVar("R") T = TypeVar("T") def infer_return(callback: Callable[[T], R]) -> R: raise NotImplementedError @overload def callback(value: Literal["a"]): ... @overload def callback(value: Literal["b"]): ... @overload def callback(value: Literal["c"]): ... @overload def callback(value: Literal["d", "e"]): ... @overload def callback(value: Literal["f", "g"]): ... @overload def callback(value: Literal["h", "i"]): ... @overload def callback(value: Literal["j", "k"]): ... @overload def callback(value: object) -> object: ... def callback(value): raise NotImplementedError assert_type(infer_return(callback), object)前 8 个重载只注解了参数类型(Literal[...]),返回类型缺失;最后一个(value: object) -> object是兜底。ty 在推断R时取整个重载集合的返回类型证据,未被注解分支不贡献返回信息,于是R特化为兜底的object。assert_type(infer_return(callback), object)即断言这一点。
泛型推断在投影预算耗尽后的恢复
泛型推断存在投影(projection)预算限制。当一个元组参数中的每个元素各自匹配回调的一个重载、而组合起来的替代绑定超过投影极限时,ty 无法继续求解,会以Unknown恢复——并且default=0这个精确实参不能补足缺失的回调证据,参数顺序无论怎样都恢复为Unknown:
from typing import Callable, Literal, TypeVar, overload from typing_extensions import assert_type from ty_extensions._internal import Unknown R = TypeVar("R") T = TypeVar("T") U = TypeVar("U") V = TypeVar("V") def infer_return(callback: tuple[Callable[[T], R], Callable[[U], R], Callable[[V], R]], default: R) -> R: raise NotImplementedError @overload def callback(value: Literal[0, 1]): ... @overload def callback(value: Literal[2, 3]): ... @overload def callback(value: Literal[4, 5]): ... @overload def callback(value: Literal[6, 7]): ... @overload def callback(value: Literal[8, 9]): ... @overload def callback(value: Literal[10, 11]): ... @overload def callback(value: Literal[12, 13]): ... @overload def callback(value: Literal[14, 15]): ... @overload def callback(value: Literal[16, 17]): ... @overload def callback(value: Literal[18, 19]): ... @overload def callback(value: object) -> object: ... def callback(value): raise NotImplementedError assert_type(infer_return((callback, callback, callback), 0), Unknown) assert_type(infer_return(default=0, callback=(callback, callback, callback)), Unknown)元组里三个位置各自独立匹配callback的重载集合,每个匹配都产生替代绑定,组合后超出 ty 泛型推断的投影预算;R无法收敛到单一类型,于是保守地返回Unknown(见 crates/ty_vendored/ty_extensions/_internal.pyi 中对Unknown的定义:缺少类型信息时推断出的动态类型)。这个用例同时锁定"预算耗尽 → 有序恢复为Unknown"的行为,防止未来实现退化为错误类型。
高阶泛型可调用对象的多次出现:fresh 类型变量
同一个泛型 callable 在高阶调用中出现多次时,每次出现都应获得独立的 fresh 类型变量:
from typing import Callable, TypeVar A = TypeVar("A") B = TypeVar("B") C = TypeVar("C") X = TypeVar("X") Y = TypeVar("Y") def partial(c: Callable[[A, B], C], a: A) -> Callable[[B], C]: def inner(b: B) -> C: return c(a, b) return inner def drop(x: X, y: Y) -> Y: return y # TODO: revealed: Literal["x"] # We are correctly combining the constraint sets from both arguments of the outer # `partial(partial, drop)` call: one from passing `partial` as `c`, and one from passing `drop` as # `a`. However, we do that after having existentially quantified away the typevars from the generic # `partial` when it's used as an argument, so this remains `Unknown` even after generic callable # occurrences are freshened. reveal_type(partial(partial, drop)(1)("x")) # revealed: Unknown # TODO: revealed: Literal[1] reveal_type(partial(partial, drop)("x")(1)) # revealed: Unknown外层partial(partial, drop)中,内层partial作为第一个实参(对应c)、drop作为第二个实参(对应a)。ty 能够把两个参数产生的约束集合并(一个来自partial传给c,一个来自drop传给a),但因为partial作为参数使用时其类型变量已被存在量化掉,即使在 fresh 化之后,结果仍是Unknown。文档中的TODO注释表明这属于已知改进项——它精确记录了"约束集已合并正确,但存在量化时机导致精度损失"的现状,是规格文档对已知局限的透明标注。
ParamSpec 替换保留非渐进变参
把变参类型特化为Any后再代入ParamSpec,不应让参数列表变成渐进(gradual)的:
from typing import Any, Callable, Generic, ParamSpec, TypeVar from ty_extensions import static_assert from ty_extensions._internal import TypeOf, is_subtype_of P = ParamSpec("P") T = TypeVar("T") class C(Generic[T]): def method(self, *args: T, **kwargs: T) -> None: ... def identity(callback: Callable[P, None]) -> Callable[P, None]: return callback callback = identity(C[Any]().method) reveal_type(callback) # revealed: (*args: Any, **kwargs: Any) -> None static_assert(is_subtype_of(TypeOf[callback], Callable[[], None]))C[Any]().method的变参类型为*args: Any, **kwargs: Any。把该 callable 代入ParamSpec时,即便特化为Any,得到的签名仍然是确定性的(*args: Any, **kwargs: Any) -> None——它依然是非渐进的普通签名,因此static_assert(is_subtype_of(TypeOf[callback], Callable[[], None]))成立(无参数调用合法)。此用例锁定的是:ParamSpec 替换不改变"变参是否渐进"这一属性。
ParamSpec 推断保留非渐进残差参数
去掉Concatenate前缀来推断ParamSpec时,同样要保留剩余参数的渐进性:
from typing import Any, Callable, Concatenate, Generic, ParamSpec, TypeVar from ty_extensions import static_assert from ty_extensions._internal import TypeOf, is_subtype_of P = ParamSpec("P") T = TypeVar("T") class C(Generic[T]): def method(self, first: int, *args: T, **kwargs: T) -> None: ... def strip_first(callback: Callable[Concatenate[int, P], None]) -> Callable[P, None]: raise NotImplementedError callback = strip_first(C[Any]().method) reveal_type(callback) # revealed: (*args: Any, **kwargs: Any) -> None static_assert(is_subtype_of(TypeOf[callback], Callable[[], None]))method的签名是(first: int, *args: T, **kwargs: T) -> None;strip_first用Concatenate[int, P]切掉首参first: int,剩余部分推断为P。特化T = Any后剩余参数仍是(*args: Any, **kwargs: Any),是非渐进的普通变参,故子类型断言通过。两个 ParamSpec 用例共同说明:无论替换还是推断,ty 都精确跟踪变参的渐进性标记,不会因Any特化或前缀切除而丢失。
SymPy 回归测试:重载/协议形状的崩溃锁定
文档末尾是一个多文件的回归用例,源于 SymPy 的一个重载/协议形状,曾在 ty 的 overload-可赋值性路径上触发 panic(因此被缩小为 MRE 作回归锁):
from __future__ import annotations from sympy.polys.compatibility import Domain, IPolys from typing import Generic, TypeVar, overload T = TypeVar("T") class DefaultPrinting: pass class PolyRing(DefaultPrinting, IPolys[T], Generic[T]): symbols: tuple[object, ...] domain: Domain[T] def clone( self, symbols: object | None = None, domain: object | None = None, order: object | None = None, ) -> PolyRing[T]: return self @overload def __getitem__(self, key: int) -> PolyRing[T]: ... @overload def __getitem__(self, key: slice) -> PolyRing[T] | Domain[T]: ... def __getitem__(self, key: slice | int) -> PolyRing[T] | Domain[T]: symbols = self.symbols[key] if not symbols: return self.domain return self.clone(symbols=symbols) def takes_ring(x: PolyRing[int]) -> None: reveal_type(x[0]) # revealed: PolyRing[int] reveal_type(x[:]) # revealed: PolyRing[int] | Domain[int]配套的桩文件(sympy/polys/compatibility.pyi等价物)定义了Domain(Generic[T])与带重载clone/__getitem__的IPolys(Protocol[T]):
from __future__ import annotations from typing import Generic, Protocol, TypeVar, overload T = TypeVar("T") S = TypeVar("S") class Domain(Generic[T]): ... class IPolys(Protocol[T]): @overload def clone( self, symbols: object | None = None, domain: None = None, order: None = None, ) -> IPolys[T]: ... @overload def clone( self, symbols: object | None = None, *, domain: Domain[S], order: None = None, ) -> IPolys[S]: ... @overload def __getitem__(self, key: int) -> IPolys[T]: ... @overload def __getitem__(self, key: slice) -> IPolys[T] | Domain[T]: ...PolyRing同时继承DefaultPrinting、IPolys[T]与Generic[T],并重载__getitem__。ty 需要在"类同时满足协议重载集合"的复杂形状下正确推断:x[0](key: int)命中第一个重载得PolyRing[int],x[:](key: slice)命中第二个重载得PolyRing[int] | Domain[int]。这个用例验证了 overload-可赋值性路径在真实生态形状下的稳定性,是本文讨论的所有规则在一个复杂组合场景中的综合应用。
如何在本地复现这些测试
这些文档全部是可运行的规格测试,无需任何手动断言:
- 检查文档格式:markdown 代码块中的 Python 片段会被抽取为源文件(按 ruff.toml 配置以
.pyi处理),# revealed:与# error:注释即期望结果,# snapshot:则关联文档末尾的snapshot代码块做精确诊断快照比对。 - 运行 mdtest 套件(在仓库根目录执行):
cargo test -p ty_python_semantic --test mdtest若要限定范围,可加测试名过滤,例如针对
generics/legacy目录的用例。 - 配置分层:单个文档可用 TOML 块覆盖全局配置,例如
[environment] python-version = "3.12";配置会按章节层级继承与覆盖,详见 mdtest_config.md。
更细的运行入口在 crates/ty_test/src/lib.rs(mdtest::run驱动)与 crates/mdtest/src/parser.rs(文档解析),mdtest 的 Python 包装脚本为 crates/ty_python_semantic/mdtest.py,外部依赖用例(如本文 SymPy 场景所属类别)还需通过MDTEST_EXTERNAL=1等环境变量启用虚拟环境。
总结:传统语法下泛型 callable 的核心规则
围绕 callables.md 这份规格,可以提炼出 ty 对传统语法泛型可调用对象的完整语义:
- 泛型身份归属:泛型函数、泛型类与可调用实例都可携带
GenericContext;TypeVar 仅出现在返回值的Callable内时,泛型的是被返回的 callable(fresh 变量记为T'return);一旦 TypeVar 出现在参数中,泛型身份归属函数本身。 - 转换不丢泛型:
into_regular_callable(以及类型层面的RegularCallableTypeOf)规范化调用风格,但保留泛型性与特化能力。 - 命名与返回:类型别名与函数返回注解都能表达泛型
Callable,ParamSpec 版本同样成立,且返回语句按返回类型的词法形式检查。 - 重载参与推断:重载集合整体匹配目标
Callable;类型变量从首个匹配重载推断;违反 bound/constraints 的分支被剔除且与声明顺序无关;重载返回类型证据用于推断返回类型变量。 - 资源与精度边界:投影预算耗尽时有序恢复为
Unknown;高阶多次出现会 fresh 化类型变量,但存在量化时机仍可能损失精度(文档以TODO明示);Any特化与Concatenate切除均不改变变参的渐进性。 - 回归保障:真实生态(SymPy)的重载/协议形状被收编为多文件 MRE,防止 overload-可赋值性路径回归或崩溃。
这些规则通过reveal_type、generic_context、static_assert与assert_type的组合被精确锁定,读者可以随时在仓库中通过cargo test -p ty_python_semantic --test mdtest复现验证。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考