news 2026/9/10 8:35:10

ty 泛型可调用对象全解析:PEP 484 传统语法下的类型推断与特化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ty 泛型可调用对象全解析:PEP 484 传统语法下的类型推断与特化

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 传统泛型语法(TypeVarParamSpecGeneric等),与 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);
  • TypeOfUnknownstatic_assertis_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))

这段代码揭示了两个精细语义:

  1. 内部函数decorator是泛型的(GenericContext[T@decorator]),但它不是工厂函数本身——generic_context(decorator_factory)返回None,因为decorator_factory没有自己的类型参数。
  2. decorator_factory()的返回值是一个新的泛型 callable,其类型变量被标注为T'return(ty 用'return后缀表示由返回类型创建的 fresh 类型变量),且decorator_factory()(1)仍然能正确推断出Literal[1]

命名泛型 Callable 与 ParamSpec:类型别名

当 callable 形状中涉及 ParamSpec 时,同样的模式成立——类型别名可以同时携带PT

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_contextNone;再向它传"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_callableT@outside_callable;传入int_identity后返回(x: int) -> int,不再泛型,因此generic_contextNone,后续错误调用触发诊断。

重载可调用对象作为泛型 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 | bytes

f的每个重载都是(str) -> None(bytes) -> None,与Callable[[T], None]兼容;ty 取第一个匹配重载的参数类型strbytes的并集,得到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: int

infer_bound的返回类型Bounded有上界int,因此str分支的返回类型(str)违反上界,被排除;int分支成为有效特化。invalid_firstinvalid_last只是把无效分支放在首位或末位,结果均为int——说明 ty 在求解约束时会扫描全部重载、剔除违反 bound/constraints 的备选,而不是贪心地采用第一个分支。Constrainedint, 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)

实参xy都是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 wrapper

wrapper拥有两个重载:一个针对协程返回类型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特化为兜底的objectassert_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) -> Nonestrip_firstConcatenate[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同时继承DefaultPrintingIPolys[T]Generic[T],并重载__getitem__。ty 需要在"类同时满足协议重载集合"的复杂形状下正确推断:x[0]key: int)命中第一个重载得PolyRing[int]x[:]key: slice)命中第二个重载得PolyRing[int] | Domain[int]。这个用例验证了 overload-可赋值性路径在真实生态形状下的稳定性,是本文讨论的所有规则在一个复杂组合场景中的综合应用。

如何在本地复现这些测试

这些文档全部是可运行的规格测试,无需任何手动断言:

  1. 检查文档格式:markdown 代码块中的 Python 片段会被抽取为源文件(按 ruff.toml 配置以.pyi处理),# revealed:# error:注释即期望结果,# snapshot:则关联文档末尾的snapshot代码块做精确诊断快照比对。
  2. 运行 mdtest 套件(在仓库根目录执行):
    cargo test -p ty_python_semantic --test mdtest

    若要限定范围,可加测试名过滤,例如针对generics/legacy目录的用例。

  3. 配置分层:单个文档可用 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 对传统语法泛型可调用对象的完整语义:

  1. 泛型身份归属:泛型函数、泛型类与可调用实例都可携带GenericContext;TypeVar 仅出现在返回值的Callable内时,泛型的是被返回的 callable(fresh 变量记为T'return);一旦 TypeVar 出现在参数中,泛型身份归属函数本身。
  2. 转换不丢泛型into_regular_callable(以及类型层面的RegularCallableTypeOf)规范化调用风格,但保留泛型性与特化能力。
  3. 命名与返回:类型别名与函数返回注解都能表达泛型Callable,ParamSpec 版本同样成立,且返回语句按返回类型的词法形式检查。
  4. 重载参与推断:重载集合整体匹配目标Callable;类型变量从首个匹配重载推断;违反 bound/constraints 的分支被剔除且与声明顺序无关;重载返回类型证据用于推断返回类型变量。
  5. 资源与精度边界:投影预算耗尽时有序恢复为Unknown;高阶多次出现会 fresh 化类型变量,但存在量化时机仍可能损失精度(文档以TODO明示);Any特化与Concatenate切除均不改变变参的渐进性。
  6. 回归保障:真实生态(SymPy)的重载/协议形状被收编为多文件 MRE,防止 overload-可赋值性路径回归或崩溃。

这些规则通过reveal_typegeneric_contextstatic_assertassert_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 8:34:41

OpenCore Legacy Patcher 实战指南:让老 Mac 免费装上最新版 macOS

OpenCore Legacy Patcher 实战指南:让老 Mac 免费装上最新版 macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher OpenCore Legacy Patcher&…

作者头像 李华
网站建设 2026/9/10 8:34:38

激光测距模组选型指南:原理、参数与场景避坑全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 8:32:53

MCU数字电源控制实战:从Matlab补偿器到定点PID实现

1. 这不是“电源设计笔记”,而是MCU上跑通数字电源控制的实战手记 你手上那块STM32F407或者GD32E507开发板,真的只是在点灯、串口打印、跑FreeRTOS吗?我去年接手一个工业温控模块升级项目时,客户原话是:“老模拟电源板…

作者头像 李华
网站建设 2026/9/10 8:32:49

GD32 FPU启用全链路配置:从硬件使能到CMSIS-DSP加速

简介:本资源面向嵌入式开发工程师及GD32平台进阶学习者,聚焦浮点运算与数字信号处理能力的实战落地,系统解决GD32微控制器中FPU启用、CMSIS-DSP库集成与高效调用等核心问题。资源包共3个文件,含1个预编译浮点数学库(ar…

作者头像 李华