首页
/ Ruff `ty` 类型检查器 `cyclic-class-definition` 检查详解:为什么类不能(直接或间接)继承自身

Ruff `ty` 类型检查器 `cyclic-class-definition` 检查详解:为什么类不能(直接或间接)继承自身

2026-09-08 10:20:57作者:乔或婵

循环继承(cyclic inheritance / cyclic class definition)是 Python 类型系统中一种必须被静态拒绝的定义形态:即便在支持前向引用的 .pyi 桩文件里,一个类也绝不能直接或间接地继承自身。本文将围绕 ruff 仓库中 ty 类型检查引擎的诊断文档 cyclic-class-definition.md,完整说明该检查做什么、为什么必须做、它如何在仓库源码中被实现(含 class 语句与 type(...) 动态建类两条路径),以及它与 MRO、metaclass 等相关诊断的协作关系。读完你不仅能理解该诊断的判定标准,还能掌握在仓库源码中定位其声明、触发点与算法实现的方法。

该检查要解决什么问题

cyclic-class-definition 检查的是桩文件(stub file)中那些(直接或间接)继承自自身的类定义。其唯一目标是发现类继承关系中的环:

# foo.pyi
class A(B): ...  # error
class B(A): ...  # error

在上面的 foo.pyi 中,AB 为基类,而 B 又以 A 为基类,两者互为基类。由于基类表达式里的前向引用在桩文件中天然被支持,两个类「先声明后使用」本身没有问题;问题在于二者形成了一个继承环,任何一条继承链都无法被收束到 object

为什么循环继承是错误的

文档给出的核心理由非常明确:

虽然前向引用(forward references)在桩文件中被原生支持,继承环仍然是不被允许的——因为对于一个继承自身的类,无法解析出一致的 MRO(方法解析顺序,method resolution order)

这背后的含义可以拆成两层:

  1. 前向引用合法 ≠ 继承环合法。桩文件允许类名在定义完成之前被引用(例如作为另一个类的基类),这是为了让 .pyi 中的类型声明可以按任意顺序书写。但这只是把「解析」推迟到类型检查阶段,并不等于允许出现无解的继承结构。
  2. MRO 必须唯一且一致。Python 计算 MRO 的 C3 线性化算法要求继承图是一个有向无环图;一旦存在环,就无法为「该类的属性查找顺序」找到一个自洽的、有限的线性序列。因而循环继承在语义上必然失败。在普通 .py 文件中,这种定义会在类创建时直接抛出 TypeError(无法构建一致的 MRO);在 .pyi 桩文件中虽不会运行,但类型检查器仍必须将其标记为错误,否则它推导出的任何类层级关系都是自相矛盾的。

从仓库源码看,类型推导引擎之所以要稳健地识别这种环,还有一层自我保护的原因:相关源码注释明确指出「这类类定义会在运行时失败,但我们必须对它保持健壮,否则可能 panic」,也就是说环检测同时也是保证推导查询不会死循环、能够收敛的必要防线(见 static_literal.rs)。

合法与不合法的形态对比

为了准确判断「什么会被报告」,我们可以把循环继承拆成几种形态:

直接循环(报错): 类把自身写在基类列表中。

class A(A): ...

间接循环 / 双向循环(报错): 两个或多个类互为祖先,示例同文档中的 foo.pyi

更长的环(报错): 环可以跨越任意多个类。

class A(B): ...
class B(C): ...
class C(A): ...  # A -> B -> C -> A,构成闭合环

仅「受牵连」而不参与环(不因此诊断报错): 如果 X 本身不在环上,只是继承了某个处于环中的类,那么 X 的继承链同样无法求解 MRO,但源码将这种形态区分为 Inherited,并不会对 X 报告本诊断(详见下文算法小节)。可见该诊断精确聚焦于「环的参与者」本身。

源码级解析:该诊断如何被声明与触发

1. 诊断的注册与元信息

该诊断在 ty 类型检查引擎的诊断定义文件中声明并注册。注册入口在 diagnostic.rsregistry.register_lint(&CYCLIC_CLASS_DEFINITION);),而声明本身在 diagnostic.rs

declare_lint! {
    #[doc = include_str!("../../resources/lint_docs/cyclic-class-definition.md")]
    pub(crate) static CYCLIC_CLASS_DEFINITION = {
        summary: "detects cyclic class definitions",
        status: LintStatus::stable("0.0.1-alpha.1"),
        default_level: Level::Error,
    }
}

从中可以确认几个关键事实:

  • 该 lint 的用户文档正是通过 include_str! 直接内联本篇文章所依据的文档 cyclic-class-definition.md——也就是说,这类 lint_docs/*.md 是面向用户的规则说明,而非仅供 Rustdoc 渲染的内部注释(见 diagnostic.rs 的文件头注释);
  • 默认报告级别为 Error(与文档中示例标注 # error 一致),以稳定状态声明;
  • 它和兄弟诊断 CYCLIC_TYPE_ALIAS_DEFINITION(检测循环类型别名定义,对应 cyclic-type-alias-definition.md)同批注册,二者共同覆盖「类型定义层面不允许出现环」这一约束。

2. class 语句路径:静态类定义的后置检查

对于常规 class 语句产生的类,检查发生在后置推断阶段。函数 check_static_class_definitions 的职责是:在所有相关类型基本推断完成后,逐个检查静态类定义在语义上是否合法、是否会在运行时抛异常。之所以必须放在后置阶段,正如其文档注释所述:基类可以被延迟解析base classes can be deferred),只有等到大多数类型就绪后才能可靠判定。

该函数中与本诊断直接相关的逻辑如下(static_class.rs):

// Check that the class does not have a cyclic definition
if let Some(inheritance_cycle) = class.inheritance_cycle(context.db()) {
    if inheritance_cycle.is_participant()
        && let Some(builder) = context.report_lint(&CYCLIC_CLASS_DEFINITION, class_node)
    {
        builder.into_diagnostic(format_args!(
            "Cyclic definition of `{}` (class cannot inherit from itself)",
            class.name(db)
        ));
    }

    // If a class is cyclically defined, that's a sufficient error to report; the
    // following checks (which are all inheritance-based) aren't even relevant.
    return;
}

这段代码揭示了三个重要设计:

  1. 只对「环的参与者」报告inheritance_cycle.is_participant() 为真时才产生诊断,诊断消息为 Cyclic definition of <类名> (class cannot inherit from itself);报告点(range)落在 class_node 上。
  2. 短路语义:一旦判定类处于循环继承中,后续所有基于继承关系的检查(插槽冲突、NamedTuple 字段顺序、泛型基类一致性、metaclass 推导等)全部跳过——循环已经使整条继承链不可信,再检查派生问题没有意义。
  3. 该函数还要兼顾运行时会抛异常的合法性检查(MRO 与 metaclass),说明「类定义合法性」是一个整体后置校验管道,循环继承只是其中最根本的一类错误。

3. 环检测算法:如何区分参与者和受牵连者

环检测的核心实现是 inheritance_cycle

/// Return this class' involvement in an inheritance cycle, if any.
pub(crate) fn inheritance_cycle(self, db: &'db dyn Db) -> Option<InheritanceCycle> {
    if !self.has_explicit_bases(db) {
        return None;
    }
    // ...
}

其内部采用一棵带记忆化的 salsa 跟踪查询 inheritance_cycle_innercycle_initial 返回 None,从而保证即使推导自身陷入环查询也能以 None 收敛),并在其中递归遍历:

  • 对每个显式基类explicit_bases)取出对应的静态类字面量(普通类直接取 ClassLiteral,泛型类则经 GenericAliasorigin);
  • 维护两条路径记录:classes_on_stack(当前递归栈)与 visited_classes(已访问的全部基类集合);
  • 当试图把一个已在栈上的类再次压栈时,说明探测到了环;随后回溯确认起点类是否也在访问集合中
  • 最终分类返回:
    • Some(InheritanceCycle::Participant)——类自身在环上,对应 cyclic-class-definition 的诊断触发;
    • Some(InheritanceCycle::Inherited)——类不在环上,只是继承了环上的类(visited_classes 包含起点但起点不在环闭合链上);
    • None——没有环。

is_participant() 这一细粒度区分,正是「只有 class A(B) / class B(A) 这种环成员被标记 # error」而环外子类不被本诊断报告的实现依据。

4. 环检测结果如何汇入 MRO 与 metaclass 推导

环的存在会在类定义的整体合法性检查中从多个角度浮出水面,最终都收敛到同一个 CYCLIC_CLASS_DEFINITION 诊断:

  • MRO 求解返回循环错误:在 static_class.rs 中,StaticMroErrorKind::InheritanceCycle 分支会报告 Cyclic definition of <类名> (class cannot inherit from itself)
  • metaclass 求解因循环失败:在 static_class.rs 中,MetaclassErrorKind::Cycle 分支同样报告 Cyclic definition of <类名>``;
  • 动态建类同样被覆盖:对于 type(name, bases, namespace) 形式动态创建的类,其 MRO 计算若遇到 DynamicMroErrorKind::InheritanceCycle,会在调用表达式上报告 Cyclic definition of <类名>``(见 dynamic_class.rs)。相关源码注释也说明:动态类的合法性在调用表达式类型推断期间单独检查,与 class 语句的后置检查路径(post-inference)互补。

值得留意的是,MRO 的错误分类非常细致(InvalidBasesInheritanceCycleDuplicateBasesUnresolvableMro 等分别路由到 INVALID_BASECYCLIC_CLASS_DEFINITIONDUPLICATE_BASEINCONSISTENT_MRO 等不同诊断),这也解释了为何「继承环」「基类重复」「MRO 不可线性化」虽然是近亲问题,却会被拆成独立诊断分别报告——对照同目录下的 duplicate-base.mdinconsistent-mro.md 可以更清楚地看到这组诊断各自聚焦的边界。

实践要点小结

要正确地使用和规避这一检查,可以把结论压缩为四条规则:

  1. .pyi 桩文件中,前向引用可以随便写,但基类链条绝不能成环——直接自继承(class A(A))、双向继承(class A(B)class B(A))以及任何多类闭合环都会被报告为 Error 级别诊断。
  2. 环的判定是传递闭包式的:只要从某类的显式基类出发能走回该类自身,就会命中;中间隔多少层类并不影响判定。
  3. 一旦命中,后续的继承类检查会被短路:循环已导致该类的 MRO/metaclass 全部不可信,优先修复继承结构本身,其余告警自然会消失。
  4. 这一约束同时覆盖静态与动态类定义class 语句由后置推断阶段检查,type(...) 动态建类由调用推断阶段检查,两条路径的最终报告都汇聚到同一诊断。

如果你希望进一步了解该诊断在类型系统中的位置,可以从 ty_python_semantic 的 lint 文档目录 出发——它集中存放了 ty 类型检查引擎全部规则的说明(同一目录下还有与继承/MRO 相关的 duplicate-base.mdinconsistent-mro.mdinvalid-base.mdcyclic-type-alias-definition.md 等),再对照 diagnostic.rs 中每个 declare_lint!include_str!summary,即可系统梳理由「继承环」扩展出的整组类定义合法性检查。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389