news 2026/9/10 9:55:55

ruff ty 类型检查器规则解析:`@final` 不得用于非方法函数(final-on-non-method)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ruff ty 类型检查器规则解析:`@final` 不得用于非方法函数(final-on-non-method)

ruff ty 类型检查器规则解析:@final不得用于非方法函数(final-on-non-method)

【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff

@final是 Python 类型系统里用来表达"禁止继承/禁止覆写"语义的关键装饰器,但它并非对任何函数都有效:把它贴到模块级函数或嵌套函数上,不会产生任何运行时与类型层面的约束,只会让代码意图落空。本文以 ruff 仓库中ty(基于 Rust 的高性能类型检查器)内置的final-on-non-method规则为对象,讲解该规则的检测目标、触发条件、消息格式、源码实现与正确写法,帮助你写出语义自洽的类型标注代码,也能让读者理解在 ruff 项目中@final相关规则的实现与测试思路。

本规则在类型检查器ty中默认启用(级别为 Error),定位在 final-on-non-method.md 这一规则文档中,可直接作为开发与教学的一手资料使用。

一、规则是什么:检测施加在非方法函数上的@final

规则文档开宗明义(crates/ty_python_semantic/resources/lint_docs/final-on-non-method.md):

  • What it does:检查@final装饰器是否被应用到了非方法(non-method)函数上;
  • Why is this bad@final装饰器只在**方法(method)类(class)**上才有意义。把它应用到模块级函数或嵌套函数上不会有任何效果,且很可能是写作者的失误。

从声明源码看,规则注册在 crates/ty_python_semantic/src/types/diagnostic.rs:

declare_lint! { #[doc = include_str!("../../resources/lint_docs/final-on-non-method.md")] pub(crate) static FINAL_ON_NON_METHOD = { summary: "detects `@final` applied to non-method functions", status: LintStatus::stable("0.0.20"), default_level: Level::Error, } }

值得注意的工程细节是:该 Markdown 文档不是游离于代码之外的说明书,而是通过include_str!直接内嵌为规则的 doc 注释,再在 diagnostic.rs 中通过registry.register_lint(&FINAL_ON_NON_METHOD)完成注册。也就是说,lint_docs 目录下的每份规则文档与代码中的 Lint 一一对应,既是文档又是 Rust 编译单元的一部分。

规则元数据汇总如下:

项目
规则代码(诊断输出中的标识)final-on-non-method
静态变量名FINAL_ON_NON_METHOD
summary检测被应用到非方法函数上的@final
默认级别Error(默认开启,报错级)
状态stable("0.0.20")(自该版本起稳定)

二、典型误用示例与诊断消息

规则文档给出的最小示例直接命中"模块级函数"这一场景(final-on-non-method.md):

from typing import final # @final is not allowed on non-method functions @final # error def my_function() -> int: return 0

ty的类型检查测试语料中,@final施加到非方法函数的错误被更细致地展开为三类场景(crates/ty_python_semantic/resources/mdtest/final.md):

from typing import final @final # error: [final-on-non-method] "`@final` cannot be applied to non-method function `func1`" def func1(): ... # Nested function decorated with `@final` is also invalid def outer(): @final # error: [final-on-non-method] def inner(): ... # A function nested inside a method is also not a method class F: def method(self): @final # error: [final-on-non-method] def not_a_method(): ...

可以看到ty实际输出的错误代码为[final-on-non-method],完整消息为`@final` cannot be applied to non-method function `func1`。这里有个非常容易误解的边界

  • 模块顶层函数(func1)→ 报错;
  • 普通函数体内定义的嵌套函数(inner)→ 报错;
  • 方法体内定义的嵌套函数(not_a_method)→ 同样报错!因为它的直接宿主作用域是"方法这个函数作用域",而不是"类作用域"。所谓 method,指的是直接声明在类体中的函数成员,方法体内的局部函数并不算方法。

三、为什么@final只对方法和类有意义

typing.final/typing_extensions.final的语义是"标记一个方法或类为 final,禁止子类覆写/继承"。类型系统里它的可检验语义只有两类:

  1. 禁止对 final 方法做覆写(override)——由override-of-final-method规则负责;
  2. 禁止继承 final 类——由subclass-of-final-class规则负责。

一个模块级函数或嵌套函数既没有"子类覆写"这一概念,也没有"继承者"概念;@final不会影响其可调用性、参数检查或返回值推断。因此把它写在非方法函数上,等于向读者宣称一条不存在且永远不会被验证的约束——检查器将其视为错误,是合理的保守选择。

在 crates/ty_python_semantic/src/types/diagnostic.rs 中可以看到与final语义相关的完整规则族:

规则代码summary关注点
override-of-final-method检测对 final 方法的覆写子类不可覆写 final 方法
override-of-final-variable检测对Final类变量的覆写子类不可覆写 final 类变量
subclass-of-final-class检测 final 类的子类final 类不可被继承
ineffective-final检测类型检查器无法解释的final()调用无效的final表达
final-without-value检测没有赋值的Final声明Final声明形式错误
abstract-and-final-method检测既 abstract 又 final 的方法两种语义互斥

final-on-non-method正是这个规则族中负责"收窄@final合法使用范围"的一环:它不负责验证 final 语义是否被破坏,而是从源头拦截"把@final用错了对象"的低级错误。

四、合法用法:方法和类

与上述误用相对,@final在以下位置是合法的:

from typing import final # 1) 普通方法(实例方法) class Service: @final def run(self) -> None: ... # 2) 类方法 / 静态方法 / 属性——装饰器顺序无关紧要 @final @classmethod def create(cls) -> "Service": ... @final @property def version(self) -> str: ... # 3) 构造方法同样适用(禁止子类改写 __init__) @final def __init__(self) -> None: ... # 4) 类 @final class ImmutableConfig: pass

以上合法性均可以从ty自身的测试断言得到印证。例如 mdtest/final.md 中的父类同时用@final标注了实例方法、property(多种装饰器顺序)、@classmethod@staticmethod,且这些声明均产生final-on-non-method错误;而子类对它们的覆写则统一触发[override-of-final-method]。这组对照测试清楚地说明了两类规则的职责分工:前者把守装饰器放置位置,后者把守覆写行为。

此外,测试还覆盖了@final@overload的组合规则(mdtest/final.md):stub 文件里@final应放在第一个 overload 上,运行期文件里@final应只放在实现函数上——放错位置会触发[invalid-overload]而非本规则。

五、源码级实现:ty如何在推断函数时触发该诊断

final-on-non-method的触发点位于函数类型推断构造器 crates/ty_python_semantic/src/types/infer/builder/function.rs:

// Check for `@final` applied to non-method functions. // `@final` is only meaningful on methods and classes. if let Some(final_decorator) = final_decorator && !self .index .scope(self.scope().file_scope_id(db)) .kind() .is_class() && let Some(builder) = self .context .report_lint(&FINAL_ON_NON_METHOD, final_decorator) { let mut diagnostic = builder.into_diagnostic(format_args!( "`@final` cannot be applied to non-method function `{name}`", )); diagnostic.info("`@final` is only meaningful on methods and classes"); }

实现要点可以拆解为三层:

  1. 识别@final装饰器。在遍历装饰器列表(function.rs)时,若某装饰器推导出的类型是Type::FunctionLiteral且其known归属为KnownFunction::Final,就把它记为final_decorator。由于final的"知名函数"识别同时覆盖typing.finaltyping_extensions.final,两种导入来源都会命中,这一点在 mdtest/final.md 中被同时验证(typingtyping_extensions各有用例)。

  2. 判断函数所处的作用域类别self.scope().file_scope_id(db)拿到的是定义这条函数语句所在作用域:模块级函数处于 module scope,嵌套函数处于外层函数(method 或 function)作用域,只有直接写在类体里的成员函数其所在作用域才是 class scope。代码据此用.kind().is_class()判负——不是类作用域,就说明这不是一个"方法"。

  3. 生成诊断。命中后通过report_lint(&FINAL_ON_NON_METHOD, final_decorator)上报,并把诊断锚点(span)落在@final装饰器本身,而不是函数名上,便于用户在长函数前快速定位出错的那一行装饰器;消息体带上函数名,附注(info)则说明正确语义。

从代码结构可以推断,@final放在普通(非类)作用域内会被直接拦截并continue跳过后续装饰器处理,因此该函数不会携带 final 标记进入任何覆写/继承判定——这与规则文档"施加无效果"的表述一致:错误在声明处即被捕获,不会留下隐患蔓延到下游检查。

六、如何触发与验证:mdtest 测试与 CLI

本仓库把文档、源码、测试三者的关系组织得很清晰:

  • lint_docs/下的 Markdown:规则的人类可读文档(即本文主体),被include_str!嵌入代码;
  • mdtest/下的 Markdown:规则的可执行测试语料。其格式约定见 crates/ty_test/README.md,通过 resources/README.md 可知它们由tests/mdtest.rs集成测试执行;
  • mdtest/测试通过行内注释断言结果,语法形如# error: [rule-code] "message"(解析逻辑见 crates/mdtest/src/assertion.rs),规则文档中的代码块同样遵守这套断言注释。

如果你想在自己的项目中复现本规则,ty检查器就构建在本仓库的 crates/ty 中,其 CLI 提供了checkexplain子命令(入口见 crates/ty/src/lib.rs,主程序见 crates/ty/src/main.rs)。大致用法:

# 对当前目录执行类型检查,命中 final-on-non-method 时会以 Error 级别报出 ty check . # 查看某条规则的说明文档 ty explain final-on-non-method

验证脚本可以这样组织:

# misuse.py from typing import final @final # error: [final-on-non-method] def helper() -> int: return 42

misuse.py运行ty check,你会得到与 mdtest/final.md 中完全一致的诊断(规则代码、消息文本、装饰器行号位置均已由 mdtest 快照锁定)。由于默认级别是Error,此类代码会被当作类型检查错误直接拦截,而不会静默通过。

七、修复方式与编写建议

该规则不提供自动修复(autofix)——ty的选择是:与其替你删除一行可能有争议的装饰器,不如把语义决策留给开发者。正确做法很简单:

  • 删除模块级/嵌套函数上的@final
  • 若函数的真实意图就是"不希望被子类覆写",那么它本应作为方法直接声明在类体中(此时@final合法且生效);
  • 若想约束的是"变量不可被覆写/重新绑定",应改用类型限定符Final(对应final-without-valueoverride-of-final-variable等规则的语义域),而不是@final

给代码审查与教学场景的三点经验:

  1. 方法 ≠ 嵌套在函数里的函数。判断依据是"定义语句所在作用域是否为类作用域"(源码判据见 function.rs),而非函数长什么样。
  2. @final的最佳位置紧贴被约束的方法/类定义。绕经lossy_decorator、多重恒等装饰器之后再识别 final 是类型检查器的实现负担(见 mdtest/final.md 的边界用例),从源头上把@final写在声明处最省心。
  3. 不要把@final@abstractmethod混用:两者语义互斥(abstract-and-final-method),抽象的必须被覆写,final 的禁止被覆写,鱼与熊掌不可兼得(mdtest/final.md)。

总结

final-on-non-method是 ruff 仓库中ty类型检查器针对@final误用场景的第一道防线。它以Error级别拦截一切施加在模块级函数与嵌套函数上的无效@final,通过与override-of-final-methodsubclass-of-final-class等规则的配合,共同保障 final 语义只在方法/类这两个真正有继承语义的对象上被表达。想要深入研究的读者,可以从三条线索继续探索本仓库:规则文档 lint_docs/final-on-non-method.md、注册声明 types/diagnostic.rs、实现与测试 types/infer/builder/function.rs 与 mdtest/final.md。

【免费下载链接】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 9:51:28

从键盘到脑机接口:人机交互输入演进与技术实践

“输入”这个词,我们天天挂在嘴边,可你仔细想过没有,从键盘敲字到现在动动嘴就能指挥设备,甚至眨眨眼睛都能完成操作,这个看似理所当然的变化,背后其实藏着一部浓缩的人机交互进化史。我做产品设计和开发这…

作者头像 李华
网站建设 2026/9/10 9:50:23

GE引擎未来路线图:昇腾AI计算架构的下一代图优化技术展望

GE引擎未来路线图:昇腾AI计算架构的下一代图优化技术展望 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率&#xff0…

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

用神经网络打造游戏大局观教练:从特征工程到实时决策

这篇不是教你怎么用AI代打,也不是给小孩省事的“外挂”。这个系列的定位更接近“陪练 复盘 策略顾问”的综合体。第一篇我们解决了让AI看懂游戏画面的基础问题,这一篇我把它升级成一个真正能“开口说话”的大局观教练——一个基于神经网络构建的、能在…

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

C语言结构体与主函数传参核心技术解析

1. C语言主函数传参与结构体深度解析在嵌入式开发和系统编程领域,C语言始终保持着不可替代的地位。最近在技术社区看到不少关于结构体初始化和参数传递的讨论,正好结合我这些年做单片机开发的经验,系统梳理下这两个核心知识点。特别是看到有S…

作者头像 李华