中 invalid-match-pattern 规则:`__match_args__` 位置子模式校验全解析)
Ruff 类型检查器基于 ty 引擎中 invalid-match-pattern 规则__match_args__位置子模式校验全解析【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruffmatch语句中的类模式class pattern是 Python 3.10 结构模式匹配最常用也最容易写错的语法之一case Point(_, _)中的每个位置子模式positional subpattern在运行时都会按照类的__match_args__逐位取属性一旦位置子模式的数量超过__match_args__的长度程序就会在运行时抛出TypeError。本文以 crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_match_pattern.md 这份测试文档为主线结合 Ruff 仓库中 ty 类型检查引擎的实现源码完整讲解invalid-match-pattern诊断规则如何静态发现这类错误包括位置子模式数量上限的计算、__match_args__类型合法性校验、继承与描述符解析、以及各种不产生诊断的边界情况。读完本文你将能够理解 ty 引擎对__match_args__的完整建模方式并能在自己的代码中准确预判哪些case分支会被静态检查拦截。一、规则概览invalid-match-pattern到底检查什么在 Ruff 的类型检查体系ty 引擎中invalid-match-patternlint 代码invalid-match-pattern负责检测会导致运行时TypeError的非法匹配模式。根据 lint 文档 的说明它覆盖五类问题在类模式中使用非类型对象如把整数 42 当作类去匹配提供位置子模式时__match_args__缺失或静态类型非法本文的核心主题对collections.abc.Callable使用位置子模式对非runtime_checkable的Protocol进行匹配对TypedDict进行匹配。其背后的实现入口位于 crates/ty_python_semantic/src/types/infer/builder.rs 的validate_class_pattern方法约 L2757。它按照以下优先级依次处理对collections.abc.Callable特殊形式直接报告位置子模式过多若类是TypedDict报告isinstance-against-typed-dict若类是非runtime_checkable的Protocol报告isinstance-against-protocol否则进入class_pattern_positional_result对__match_args__做位置子模式校验若类类型根本不是type则报告不是类型不能用于类模式。本文聚焦第 4 步即针对__match_args__的位置校验逻辑。二、核心概念__match_args__与位置子模式的绑定机制在 Python 运行时语义中类模式的case Point(x, y)等价于先做isinstance(subject, Point)判断再把Point.__match_args__中的属性名逐位取出对subject取对应属性后与x、y做子匹配。因此__match_args__的元组长度直接决定了类模式允许的位置子模式数量上限。ty 引擎在 crates/ty_python_semantic/src/types/match_pattern.rs 中把这个过程建模为两个层次class_pattern_positional_result决定位置子模式的静态校验结果上限值或非法类型class_pattern_positional_sources把每个位置子模式映射到它实际绑定的值来源定义在ClassPatternPositionalSource枚举中约 L330pub(crate) enum ClassPatternPositionalSource { /// 整个 subject 本身用于 Python 特殊内建类的类模式 MatchSelf, /// 按 __match_args__ 从 subject 中提取的命名属性 Attribute(Name), /// 静态无法确定值来源子模式不具穷尽性 Unknown, }其中MatchSelf对应int(_)、str(_)这类特殊内建类的行为——它们只有一个位置参数且该参数接收的是整个 subject而不是某个属性见 match_pattern.rs 中的文档示例。三、规则一位置子模式数量超过__match_args__长度3.1 静态定长元组的上限测试文档的第一个场景是位置子模式过多Too many positional subpatterns。其核心约束是位置子模式的数量不能超过静态已知定长__match_args__元组的长度而这个元组类型可以来自推断值、注解或其他表达式[environment] python-version 3.12from typing import Literal class Point: __match_args__ (x, y) one_arg (value,) class FromVariable: __match_args__ one_arg def make_args() - tuple[Literal[value]]: return (value,) class FromCall: __match_args__ make_args() class Annotated: __match_args__: tuple[Literal[value]] make_args() type MatchArgs tuple[Literal[value]] class Aliased: __match_args__: MatchArgs (value,)在这个示例中__match_args__的来源有五种典型写法类体内直接赋值的元组字面量Point、从模块级变量推断FromVariable、函数调用返回值FromCall、显式类型注解Annotated、以及通过类型别名注解Aliased。ty 引擎对它们都能推算出定长元组类型从而得到上限def describe( point: Point, from_variable: FromVariable, from_call: FromCall, annotated: Annotated, aliased: Aliased, ) - None: match point: case Point(_, _): # 合法等于上限 2 pass match point: # error: [invalid-match-pattern] Too many positional subpatterns for class Point: expected 2, got 3 case Point(_, _, _): pass match from_variable: case FromVariable(_, _): # error: [invalid-match-pattern] expected 1, got 2 pass match from_call: case FromCall(_, _): # error: [invalid-match-pattern] expected 1, got 2 pass match annotated: case Annotated(_, _): # error: [invalid-match-pattern] expected 1, got 2 pass match aliased: case Aliased(_, _): # error: [invalid-match-pattern] expected 1, got 2 pass注意错误信息统一为Too many positional subpatterns for \class Point: expected {limit}, got {count}其中{limit}是静态推算出的match_args长度{count}是源码中实际书写的位置子模式数量。诊断由 [diagnostic.rs](https://link.gitcode.com/i/fc6690c8a3b8c0215ab024c2716fd6a1) 中的report_too_many_positional_patterns_for_class_pattern 生成。3.2 缺少__match_args__与内建类的 match-self 行为__match_args__完全缺失时普通源码类不接受任何位置子模式而具有 match-self 行为的内建类如int接受一个其他已知内建类不接受任何位置子模式class Missing: ... def describe(missing: Missing, integer: int, complex_number: complex) - None: match missing: case Missing(_): # error: [invalid-match-pattern] expected 0, got 1 pass match integer: case int(_): # 合法match-self接受 1 个 pass match integer: case int(_, _): # error: [invalid-match-pattern] expected 1, got 2 pass match complex_number: case complex(_): # error: [invalid-match-pattern] expected 0, got 1 pass这条逻辑在源码中有精确的对应实现。class_has_match_self_flagmatch_pattern.rs沿 MRO 检查类是否继承自bool、bytearray、bytes、dict、float、frozenset、int、list、set、str、tuple这些已知类而class_pattern_positional_resultmatch_pattern.rs则在__match_args__未定义时据此返回不同的上限具有 match-self 标志 →Limit(1)是已知类或静态类且其定义文件不是 stub →Limit(0)否则返回None不产生诊断。3.3 数据类与命名元组的自动生成行为dataclass与NamedTuple会自动合成__match_args__其中仅位置字段参与。文档明确列出了三种需要注意的情况dataclass(match_argsFalse)会关闭合成等价于没有__match_args__dataclass(kw_onlyTrue)的关键字专属字段不参与合成通过field(kw_onlyTrue)标记的字段同样不参与合成。from dataclasses import dataclass, field from typing import NamedTuple dataclass class Point: x: int y: int dataclass(match_argsFalse) class NoMatch: x: int y: int dataclass(kw_onlyTrue) class KeywordOnly: x: int dataclass class PartlyKeywordOnly: x: int y: int field(kw_onlyTrue) class NamedPoint(NamedTuple): x: int y: int def describe( point: Point, no_match: NoMatch, keyword_only: KeywordOnly, partly_keyword_only: PartlyKeywordOnly, named_point: NamedPoint, ) - None: match point: case Point(_, _, _): # error: [invalid-match-pattern] expected 2, got 3 pass match no_match: case NoMatch(_, _): # error: [invalid-match-pattern] expected 0, got 2 pass match keyword_only: case KeywordOnly(_): # error: [invalid-match-pattern] expected 0, got 1 pass match partly_keyword_only: case PartlyKeywordOnly(_, _): # error: [invalid-match-pattern] expected 1, got 2 pass match named_point: case NamedPoint(_, _, _): # error: [invalid-match-pattern] expected 2, got 3 passKeywordOnly只有关键字字段所以合成出的__match_args__为空元组上限 0PartlyKeywordOnly只有x参与上限 1。这也提醒我们在设计数据类时若字段以kw_only为主case分支应优先使用关键字模式如case Point(xx)而不是位置子模式。四、规则二__match_args__类型非法非精确元组当__match_args__的静态类型确定不是元组时任何位置子模式都无法成立规则会报告必须是精确元组from typing_extensions import LiteralString bad_args [value] class FromVariable: __match_args__ bad_args def make_args() - str: return value class FromCall: __match_args__ make_args() class Annotated: __match_args__: int 1 class Position: __match_args__: LiteralString field def describe( from_variable: FromVariable, from_call: FromCall, annotated: Annotated, position: Position, ) - None: match from_variable: # error: [invalid-match-pattern] must be an exact tuple, not list[str] case FromVariable(_): pass match from_call: # error: [invalid-match-pattern] must be an exact tuple, not str case FromCall(_): pass match annotated: # error: [invalid-match-pattern] must be an exact tuple, not int case Annotated(_): pass match position: # error: [invalid-match-pattern] must be an exact tuple, not LiteralString case Position(_): passlist[str]、str、int、LiteralString都被判定为非法。在运行时TypeError会在真正尝试按位置取值时才触发因此这类错误往往隐藏很深静态检查的价值就在这里。诊断消息\match_args for {class_display} must be an exact tuple, not {match_args_display}由 [diagnostic.rs](https://link.gitcode.com/i/9c6c27ec479f7dd56fffee1bdbec2368) 的report_invalid_match_args_type 生成。从实现上看判定非法类型的关键是 match_pattern.rs 中的这段逻辑当__match_args__不是定长元组时检查它是否与任意元组类型不相交is_disjoint_from——若不相交说明它确定是非元组才报告InvalidType若相交则说明类型尚不确定可能是元组保持静默。五、规则三语义成员查找——继承与描述符ty 对__match_args__的解析使用的是语义成员类型semantic member type而非简单地读取类体赋值因此继承的__match_args__和描述符descriptor提供的值都会被正确解析from typing import Literal, final class Base: __match_args__ (value,) class Derived(Base): ... final class MatchArgsDescriptor: def __get__(self, instance: object | None, owner: type[object]) - tuple[Literal[value]]: return (value,) class Descriptor: __match_args__ MatchArgsDescriptor() def describe(derived: Derived, descriptor: Descriptor) - None: match derived: case Derived(_, _): # error: [invalid-match-pattern] expected 1, got 2 pass match descriptor: case Descriptor(_): # 合法描述符返回定长 1 元组 pass match descriptor: case Descriptor(_, _): # error: [invalid-match-pattern] expected 1, got 2 passDerived虽未显式定义__match_args__但通过继承获得上限 1Descriptor的__match_args__是描述符实例ty 通过__get__的返回类型tuple[Literal[value]]推算出上限 1。底层实现是class_match_args_typematch_pattern.rs它通过Type::ClassLiteral(class).member(db, env, __match_args__)执行完整的成员解析并区分三种状态Defined确定定义进一步区分成员类型用于校验与位置来源类型用于子模式到属性的映射。这里有一个微妙之处显式注解保持权威origin.is_declared()时直接用注解类型而推断赋值则保留其字面量绑定类型PossiblyUndefined条件定义运行时行为不确定不参与上限推算Undefined完全缺失此时才可能启用 match-self 行为。注释里特别强调PossiblyUndefined与Undefined的区别至关重要因为只有真正缺失__match_args__才能触发 match-self 行为一旦存在哪怕是条件性的定义match-self 就不适用。六、规则的静默边界哪些情况不产生诊断6.1 未知上限类型无法确定定长或非元组当成员类型既无法确定定长元组长度、也无法确定非元组时规则保持静默——因为运行时行为依赖实际值from typing import Literal class Variadic: __match_args__: tuple[str, ...] () # 变长元组长度未知 class Mixed: __match_args__: tuple[Literal[value]] | list[str] (value,) # 联合类型 class TupleSubclass(tuple[str]): ... class SubclassValue: __match_args__: TupleSubclass # 元组子类 def describe( variadic: Variadic, mixed: Mixed, subclass_value: SubclassValue, ) - None: match variadic: case Variadic(_, _): pass match mixed: case Mixed(_, _): pass match subclass_value: case SubclassValue(_): pass这三类分别对应变长元组tuple[str, ...]无定长、元组与非元组的联合不确定、元组子类同样无法确认精确元组实例规格。对应到实现上exact_tuple_instance_spec取不到定长时返回Noneis_disjoint_from对混合类型返回不相交才报告InvalidType而tuple[...] | list[str]与元组有交集故静默。6.2 缺失 stub 成员如果__match_args__的缺失发生在 stub 文件中.pyi同样不建立运行时上限。文档中的例子是lib.pyi中声明class Model: ...而业务代码中from lib import Model def describe(model: Model) - None: match model: case Model(_): passstub 中完全省略__match_args__视为未定义而依据 match_pattern.rs 的判断Limit(0)只对非 stub 文件中的静态类成立对来自 stub 的类class_has_match_self_flag为假于是返回None保持静默。6.3 没有位置子模式的模式不带位置子模式的类模式如case Model()和纯关键字模式如case Model(value_)不会检查__match_args__class Model: __match_args__ [value] # 注意这里甚至是 list类型非法 value: int 0 def describe(value: Model) - None: match value: case Model(): pass match value: case Model(value_): pass尽管Model.__match_args__是list按第四节标准属非法类型但因为没有位置子模式规则完全不介入——这与运行时语义一致__match_args__只在出现位置子模式时才被使用。6.4 非法类模式不级联优先级问题当类本身不能用于匹配时TypedDict、非runtime_checkable的Protocol先报告更高优先级的错误不再叠加位置子模式相关的诊断from typing import Protocol, TypedDict class Payload(TypedDict): value: int class HasValue(Protocol): value: int def describe(value: object) - None: match value: # error: [isinstance-against-typed-dict] case Payload(_): pass match value: # error: [isinstance-against-protocol] case HasValue(_): pass在validate_class_pattern中TypedDict与 Protocol 的检查发生在位置校验之前并直接return这正是不级联的实现保证。七、源码验证路径与扩展阅读规则主体测试用例crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_match_pattern.md规则 lint 文档crates/ty_python_semantic/resources/lint_docs/invalid-match-pattern.md诊断消息实现crates/ty_python_semantic/src/types/diagnostic.rsreport_too_many_positional_patterns_for_class_pattern位于 L3636report_invalid_match_args_type位于 L3652校验调度入口validate_class_patterncrates/ty_python_semantic/src/types/infer/builder.rs位置子模式校验核心crates/ty_python_semantic/src/types/match_pattern.rsClassPatternPositionalResult位于 L407class_pattern_positional_result位于 L415class_match_args_type位于 L346class_has_match_self_flag位于 L382相关匹配语义的测试还分布在 narrow/match.md 与 conditional/match.md 中可交叉阅读以理解匹配对类型收窄的影响。ty 引擎的整体架构与运行方式可参考 crates/ty/README.md。总结invalid-match-pattern规则把 Python 结构模式匹配中最容易在运行时引爆TypeError的__match_args__场景转化为可在编译期静态判定的规则。它的核心结论可以浓缩为四句话位置子模式数量不得超过静态定长__match_args__的长度__match_args__静态类型必须是非精确元组以外的合法定长元组dataclass/NamedTuple的合成行为会改变上限match_argsFalse与kw_only字段均被排除而当类型信息不足变长元组、联合、stub 缺失或模式不含位置子模式时规则选择静默。理解这套建模方式不仅能在使用 Ruff 时看懂每一条invalid-match-pattern报错也能帮你写出对类型检查器更友好、更少运行时意外的match代码。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考