news 2026/9/10 7:41:58

ty 类型检查器中的 del 语句语义:从名称删除到属性与下标删除的完整类型建模

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ty 类型检查器中的 del 语句语义:从名称删除到属性与下标删除的完整类型建模

ty 类型检查器中的 del 语句语义:从名称删除到属性与下标删除的完整类型建模

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

导读

本文围绕 ruff 仓库中 ty 类型检查器的del语句语义测试文档(del.md)展开,系统讲解类型检查器如何处理 Python 的del语句:包括名称删除后的未解析引用检测、del对局部作用域解析的强制影响、属性删除(property deleter、__delattr__、描述符__delete__、Final 属性、元类)以及下标删除(__delitem__与 TypedDict 键删除规则)。读完本文,你将掌握 ty 在del上的完整类型规则、每条规则对应的诊断码(如unresolved-referenceinvalid-assignmentnot-subscriptableinvalid-argument-type),以及这些规则在仓库源码中的落地位置,可直接用于理解或扩展类型检查行为。

del语句与 mdtest:这份文档是什么

crates/ty_python_semantic/resources/mdtest/del.md是 ty 语义分析模块的 mdtest 测试文档。它把"可执行代码 + 期望诊断"写在同一个 Markdown 文件中:代码块中的# error: [诊断码]注释声明该行应当触发指定诊断,reveal_type(x) # revealed: T声明表达式x的推断类型为T,而# snapshot: <name>配合独立snapshot代码块用于校验多行诊断快照的精确文本。

文档覆盖了三条主线,对应本文的三个核心章节:

  1. 名称删除del adel x, y):作用域绑定关系、可达性与未解析引用;
  2. 属性删除del obj.attr):property、描述符、__delattr____delete__、Final、元类等一整套协议;
  3. 下标删除del obj[key]):__delitem__独立性、TypedDict 键的可删除性判定。

名称删除:绑定生命周期与未解析引用

基本删除与多目标删除

del从当前作用域移除一个名称的绑定。删除后再引用该名称,ty 报告unresolved-reference

a = 1 del a # error: [unresolved-reference] reveal_type(a) # revealed: Unknown

对同一个变量执行两次删除(第二次删除一个已经不存在的绑定)同样报错:

# error: [invalid-syntax] "Invalid delete target" del 1 # error: [unresolved-reference] del a

注意第一行的del 1:删除目标必须是名称、属性或下标等可删除目标,直接删除字面量属于语法错误,ty 报invalid-syntax且诊断消息为 "Invalid delete target"。

del支持一次删除多个目标,每个目标独立处理绑定:

x, y = 1, 2 del x, y # error: [unresolved-reference] reveal_type(x) # revealed: Unknown # error: [unresolved-reference] reveal_type(y) # revealed: Unknown

条件删除与 possibly-unresolved-reference

del位于条件分支内时,删除是否真正生效取决于分支是否执行,此时绑定状态是"可能未定义",ty 使用possibly-unresolved-reference表达这种不确定性:

def cond() -> bool: return True b = 1 if cond(): del b # error: [possibly-unresolved-reference] reveal_type(b) # revealed: Literal[1]

b的静态类型仍保留为Literal[1](赋值时推断出的字面量类型),但它的绑定状态是可能已被删除,因此访问时报possibly-unresolved-reference而非unresolved-reference——两者的差别正是流分析对"必然未定义"与"可能未定义"的区分。

对称地,当某个分支不删除变量、另一个分支删除它时,删除路径仍是可能的:

c = 1 if cond(): c = 2 else: del c # error: [possibly-unresolved-reference] reveal_type(c) # revealed: Literal[2]

函数内的 del 与外部变量:按名称而非按值删除

del删除的是绑定关系,而非改变对象本身。模块级变量d在函数内部被删除时,ty 将其视为对该名称的引用:

d = [1, 2, 3] def delete(): del d # error: [unresolved-reference] "Name `d` used when not defined" delete() reveal_type(d) # revealed: list[int]

函数delete内部没有global d声明,因此del d使d成为delete的局部名称;而函数体内引用局部名称时该名称在函数作用域中"未定义",所以报unresolved-reference。模块作用域中的d不受影响,仍为list[int]

当删除目标不是名称时,del不会强制局部解析。删除d[0]只是对d的元素做删除操作,d本身按外层作用域解析:

def delete_element(): # When the `del` target isn't a name, it doesn't force local resolution. del d[0] print(d)

global / nonlocal 声明下的重复删除

一旦使用globalnonlocal声明,del操作的是外部作用域的绑定。ty 对这些场景采取保守策略:虽然在此平凡示例中可以推断出变量已未绑定,但因为变量可能在两次del之间被重新初始化,ty 选择不跟踪全局/非局部绑定的"未绑定"状态,以避免误报:

def delete_global(): global d del d # We could lint that `d` is unbound in this trivial case, but because it's global we'd need to # be careful about false positives if `d` got reinitialized somehow in between the two `del`s. del d delete_global() # Again, the variable should have been removed, but we don't check it. reveal_type(d) # revealed: list[int]

nonlocal场景同理:

def delete_nonlocal(): e = 2 def delete_nonlocal_bad(): del e # error: [unresolved-reference] "Name `e` used when not defined" def delete_nonlocal_ok(): nonlocal e del e # As with `global` above, we don't track that the nonlocal `e` is unbound. del e

注意delete_nonlocal_bad内没有nonlocal e声明,del e使e成为该函数局部名且未定义,故报错;delete_nonlocal_ok声明了nonlocal,两次del e都合法。

del 与局部作用域解析:强制 vs 例外

即使不可达,del 也强制局部解析

这是del语义中比较微妙的一点:只要函数foo中存在del x(哪怕位于if False:这类不可达分支中),x就成为foo的局部名称,而局部名称又被判定为未定义,于是连内层函数bar中对x的引用也解析到foo的绑定并报错:

x = 1 def foo(): print(x) # error: [unresolved-reference] "Name `x` used when not defined" if False: # Assigning to `x` would have the same effect here. del x def bar(): print(x) # error: [unresolved-reference] "Name `x` used when not defined"

这与"对x赋值具有相同效果"——Python 的作用域规则中,任何对名称的绑定操作(赋值、delimport等)都会使其成为所在函数的局部变量,del与赋值在此处行为一致。文档注释也明确说明了这一点。

global / nonlocal 变量不受 del 强制局部解析

global x声明会改变上述行为:foo中的del x不再使x局部化,bar中的print(x)解析到全局作用域,模块级的x类型保持不变:

x = 1 def foo(): global x def bar(): # allowed, refers to `x` in the global scope reveal_type(x) # revealed: Literal[1] bar() del x # allowed, deletes `x` in the global scope (though we don't track that)

nonlocal x具有类似效果。给nonlocal一个可引用的enclosing作用域后,del x合法,且内层引用解析到enclosing中的绑定:

def enclosing(): x = 2 def foo(): nonlocal x def bar(): # allowed, refers to `x` in `enclosing` reveal_type(x) # revealed: Literal[2] bar() del x # allowed, deletes `x` in `enclosing` (though we don't track that)

删除属性:协议、窄化失效与限制

属性删除比名称删除复杂得多。ty 从源码结构看,将属性删除校验集中在 builder.rs 的validate_attribute_deletion中,其处理顺序为:拆解联合类型 → 查找并调用__delattr__→ 校验 Final 属性 → 检查属性值类型上的描述符__delete__→ 检查 property deleter。下面按文档示例逐一说明。

删除属性使窄化失效

属性被删除后,如果之后再被引用,运行时会报错;但 ty把"删除后引用"当作静态错误——因为删除语句与方法调用之间可能存在重新定义。不过删除属性会使赋值窄化(narrowing)失效,属性类型回退到其原始声明类型:

class C: x: int = 1 c = C() del c.x reveal_type(c.x) # revealed: int

删除不存在的属性会报错(unresolved-attribute):

# error: [unresolved-attribute] del c.non_existent

窄化失效可以反复发生:赋值使类型收窄为Literal[1],删除后再次回退为声明的int

c.x = 1 reveal_type(c.x) # revealed: Literal[1] del c.x reveal_type(c.x) # revealed: int

删除实例属性定义(类级删除)

在类上执行del C.x删除的是类属性定义。ty 对由此导致的实例属性不可解析暂不检查(因为后续C()构造或运行时行为可能不同),实例访问的类型仍按原声明推断:

class C: x: int = 1 c = C() reveal_type(c.x) # revealed: int del C.x c = C() # This attribute is unresolved, but we won't check it for now. reveal_type(c.x) # revealed: int

Property deleters:read-only 属性不可删除

只定义了 getter 而没有 deleter 的@property是只读属性,删除报invalid-assignment。定义了@x.deleter的属性可以删除;若 deleter 返回NoReturn(声明其必然抛异常,如raise AttributeError),删除同样报错:

from typing import NoReturn class ReadOnlyProperty: @property def x(self) -> int: return 1 class SupportsDelete: @property def x(self) -> int: return 1 @x.deleter def x(self) -> None: pass class RejectsDescriptorDelete: @property def x(self) -> int: return 1 @x.deleter def x(self) -> NoReturn: raise AttributeError("x") class ExplicitNoneDeleter: def get(self) -> int: return 1 keyword = property(get, fdel=None) positional = property(get, None, None) read_only = ReadOnlyProperty() # error: [invalid-assignment] "Cannot delete read-only property `x` on object of type `ReadOnlyProperty`" del read_only.x supports_delete = SupportsDelete() del supports_delete.x rejects_descriptor_delete = RejectsDescriptorDelete() # error: [invalid-assignment] "Cannot delete attribute `x` on type `RejectsDescriptorDelete` whose `__delete__` method returns `Never`/`NoReturn`" del rejects_descriptor_delete.x explicit_none_deleter = ExplicitNoneDeleter() # error: [invalid-assignment] "Cannot delete read-only property `keyword` on object of type `ExplicitNoneDeleter`" del explicit_none_deleter.keyword # error: [invalid-assignment] "Cannot delete read-only property `positional` on object of type `ExplicitNoneDeleter`" del explicit_none_deleter.positional

注意property(get, fdel=None)property(get, None, None)这两种传参方式都被识别为"无 deleter",等价于只读属性。源码中对应的判定逻辑是 property_deleter_returns_never:调用 deleter 后检查返回类型是否为Never,并结合__delete__的调用结果共同决定是否报错。

实例__delattr__:自定义删除行为

ty 通过调用对象的__delattr__方法来建模属性删除。若__delattr__返回None(正常接受删除),删除合法;返回NoReturn(声明必然抛AttributeError),删除报invalid-assignment;若__delattr__object.__delattr__签名不兼容(如参数类型从str变为int),定义处报invalid-method-override,删除处报invalid-assignment

from typing import NamedTuple, NoReturn class SupportsCustomDelete: @property def x(self) -> int: return 1 def __delattr__(self, name: str) -> None: pass class RejectsDelete: @property def x(self) -> int: return 1 def __delattr__(self, name: str) -> NoReturn: raise AttributeError(name) class BadDelAttr: x: int = 1 # error: [invalid-method-override] "Invalid override of method `__delattr__`: Definition is incompatible with `object.__delattr__`" def __delattr__(self, name: int) -> None: pass class DeletableNamedTuple(NamedTuple): x: int def __delattr__(self, name: str) -> None: pass supports_custom_delete = SupportsCustomDelete() del supports_custom_delete.x rejects_delete = RejectsDelete() # error: [invalid-assignment] "Cannot delete attribute `x` on type `RejectsDelete` whose `__delattr__` method returns `Never`/`NoReturn`" del rejects_delete.x bad_delattr = BadDelAttr() # error: [invalid-assignment] "Cannot delete attribute `x` on type `BadDelAttr` with custom `__delattr__` method" del bad_delattr.x deletable_namedtuple = DeletableNamedTuple(1) del deletable_namedtuple.x

从 validate_attribute_deletion 的源码可以确认:ty 用MRO_NO_OBJECT_FALLBACK策略查找__delattr__,构造CallArguments::positional([Type::string_literal(db, attribute)])调用它,当返回类型为Never时报告 "whose__delattr__method returnsNever/NoReturn"。NamedTuple只要显式定义了__delattr__,其字段删除即被允许。

描述符__delete__:签名与回退

描述符协议中__delete__负责删除行为。若描述符的__delete__签名异常(本例中带额外的extra参数),调用失败,ty 回退到实例属性删除路径,最终删除仍被允许:

class Weird: def __delete__(self, instance: object, extra: object) -> None: pass class FallbackInstanceAttribute: def __init__(self) -> None: self.x = Weird() fallback_instance_attribute = FallbackInstanceAttribute() del fallback_instance_attribute.x

源码中这一回退逻辑位于 builder.rs:先通过assignment_attribute_members找到属性的类型成员,尝试调用其__delete__MethodNotAvailable时回退放行,而CallError则通过report_bad_dunder_delete_call报告调用错误。

Final 属性:一律禁止删除

Final标记的属性不可删除。实例属性、带自定义__delattr__的实例属性、以及类属性三类场景都报invalid-assignment

from typing import Final class FinalAttribute: def __init__(self) -> None: self.x: Final[int] = 1 class FinalAttributeWithDelAttr: def __init__(self) -> None: self.x: Final[int] = 1 def __delattr__(self, name: str) -> None: pass class FinalClassAttribute: x: Final[int] = 1 final_attribute = FinalAttribute() # error: [invalid-assignment] "Cannot delete final attribute `x` on type `FinalAttribute`" del final_attribute.x final_attribute_with_delattr = FinalAttributeWithDelAttr() # error: [invalid-assignment] "Cannot delete final attribute `x` on type `FinalAttributeWithDelAttr`" del final_attribute_with_delattr.x # error: [invalid-assignment] "Cannot delete final attribute `x` on type `<class 'FinalClassAttribute'>`" del FinalClassAttribute.x

注意第三处:即使自定义__delattr__接受删除,Final 约束仍然优先,禁止删除。该校验独立实现于 final_attribute.rs 的validate_final_attribute_deletion,会遍历属性成员的所有限定符(qualifiers)检查 Final 标记。

元类__delattr__:类属性删除走元类

对类对象执行属性删除(del Cls.x)时,调用的是元类__delattr__。元类__delattr__返回None则允许删除;返回NoReturn则报invalid-assignment

from typing import NoReturn class SupportsClassDeleteMeta(type): def __delattr__(self, name: str) -> None: pass class SupportsClassDelete(metaclass=SupportsClassDeleteMeta): x: int = 1 class RejectsClassDeleteMeta(type): def __delattr__(self, name: str) -> NoReturn: raise AttributeError(name) class RejectsClassDelete(metaclass=RejectsClassDeleteMeta): x: int = 1 del SupportsClassDelete.x # error: [invalid-assignment] "Cannot delete attribute `x` on type `<class 'RejectsClassDelete'>` whose `__delattr__` method returns `Never`/`NoReturn`" del RejectsClassDelete.x

注意错误消息中对象类型显示为<class 'RejectsClassDelete'>,表明删除发生在类字面量(ClassLiteral)上,这正是元类路径的判别特征。

删除元素:__delitem__与 TypedDict 规则

基本元素删除同样使窄化失效

下标删除del l[0]与属性删除一样,会使赋值窄化失效,但元素访问本身仍然合法(列表长度运行时未知,删除后访问可能是运行时错误,却不构成静态类型错误):

def f(l: list[int]): del l[0] # If the length of `l` was 1, this will be a runtime error, # but if it was greater than that, it will not be an error. reveal_type(l[0]) # revealed: int # error: [invalid-argument-type] del l["string"] l[0] = 1 reveal_type(l[0]) # revealed: Literal[1] del l[0] reveal_type(l[0]) # revealed: int

同时,下标类型要经过参数类型校验:list[int]的下标必须是整数,del l["string"]invalid-argument-type

__delitem____getitem__相互独立

ty 从源码结构看,在下标删除的校验函数 can_delete_subscript 中只查找__delitem__,因此两个协议是独立建模的:

  • 只有__delitem__没有__getitem__:支持删除,但不支持读取(读取报not-subscriptable)。协议类与普通类都遵循此规则:
from typing import Protocol, TypeVar KT = TypeVar("KT", contravariant=True) class CanDelItem(Protocol[KT]): def __delitem__(self, k: KT, /) -> None: ... def f(x: CanDelItem[int], k: int): # This should be valid - the object has __delitem__ del x[k] class OnlyDelItem: def __delitem__(self, key: int) -> None: pass d = OnlyDelItem() del d[0] # OK # error: [not-subscriptable] "Cannot subscript object of type `OnlyDelItem` with no `__getitem__` method" d[0]
  • 只有__getitem__没有__delitem__:支持读取,但删除报not-subscriptable,诊断消息明确指出 "Cannot delete subscript ... with no__delitem__method":
class OnlyGetItem: def __getitem__(self, key: int) -> str: return "value" g = OnlyGetItem() reveal_type(g[0]) # revealed: str # error: [not-subscriptable] "Cannot delete subscript on object of type `OnlyGetItem` with no `__delitem__` method" del g[0]

TypedDict 键删除:required 不可删,NotRequired 可删

删除 TypedDict 的 required 键会使对象不再符合该 TypedDict 类型,因此是类型错误;而NotRequired键与total=FalseTypedDict 的所有键都可以删除。文档定义了三种 TypedDict:

from typing_extensions import TypedDict, NotRequired class Movie(TypedDict): name: str year: int class PartialMovie(TypedDict, total=False): name: str year: int class MixedMovie(TypedDict): name: str year: NotRequired[int] m: Movie = {"name": "Blade Runner", "year": 1982} p: PartialMovie = {"name": "Test"} mixed: MixedMovie = {"name": "Test"}

删除 required 键报invalid-argument-type,并给出完整的诊断快照(错误位置、字段定义位置与修复建议):

# snapshot: invalid-argument-type del m["name"]
error[invalid-argument-type]: Cannot delete required key "name" from TypedDict `Movie` --> src/mdtest_snippet.py:19:7 | 19 | del m["name"] | ^^^^^^ info: Field defined here --> src/mdtest_snippet.py:3:7 | 3 | class Movie(TypedDict): | ---------------- `Movie` defined here 4 | name: str | --------- | | | `name` declared as required here | Consider making it `NotRequired` info: Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted

total=False的 TypedDict 中所有键均可删除:

del p["name"]

NotRequired键始终可删除:

del mixed["year"]

但混合 TypedDict 中的 required 键仍然不可删除:

# snapshot: invalid-argument-type del mixed["name"]
error[invalid-argument-type]: Cannot delete required key "name" from TypedDict `MixedMovie` --> src/mdtest_snippet.py:23:11 | 23 | del mixed["name"] | ^^^^^^ info: Field defined here --> src/mdtest_snippet.py:11:7 | 11 | class MixedMovie(TypedDict): | --------------------- `MixedMovie` defined here 12 | name: str | --------- | | | `name` declared as required here | Consider making it `NotRequired` info: Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted

不存在的键同样不可删除:

# snapshot: invalid-argument-type del mixed["non_existent"]
error[invalid-argument-type]: Cannot delete unknown key "non_existent" from TypedDict `MixedMovie` --> src/mdtest_snippet.py:25:11 | 25 | del mixed["non_existent"] | ^^^^^^^^^^^^^^

这些规则的实现位于 subscript.rs:ty 首先尝试删除任意字面量键(允许NotRequired键或total=False),随后调用__delitem__;当键为 required 或未知时,通过report_cannot_delete_typed_dict_key分别报告 "Cannot delete required key" 与 "Cannot delete unknown key"。

源码实现总览:del 的完整处理链路

将文档示例与仓库实现对照,可以梳理出 ty 处理del语句的完整链路:

  1. 语法分发:builder.rs 将ast::Stmt::Delete路由到infer_delete_statement
  2. 目标遍历:infer_delete_statement 遍历del的每个目标并逐一infer_expression,即统一交给表达式推断,名称、属性、下标三类目标由此分别进入各自的推断逻辑;
  3. 名称目标:由绑定表(place table)与 use-def 分析共同决定unresolved-reference/possibly-unresolved-reference,并受作用域规则(global/nonlocal、函数局部化)约束;
  4. 属性目标validate_attribute_deletion依次处理__delattr__调用、Final 属性校验、描述符__delete__与 property deleter 返回类型,相关校验分散于 builder.rs 与 final_attribute.rs;
  5. 下标目标can_delete_subscript校验__delitem__存在性与参数类型,并对 TypedDict 键执行 required/NotRequired/未知键的三态判定(subscript.rs);
  6. 删除对窄化的影响:删除会收集deleted_narrowing_constraints并使绑定状态转为DefinitionState::Deleted(place.rs),这正是"删除使赋值窄化失效、类型回退到声明类型"这一贯穿全文行为的基础。

小结

del语句在 ty 类型检查器中被建模为三类目标分别处理:名称删除受作用域与可达性约束(unresolved-reference/possibly-unresolved-reference);属性删除受__delattr__、描述符__delete__、property deleter、Final 标记与元类等协议约束(invalid-assignment/unresolved-attribute/invalid-method-override);下标删除受__delitem__独立性与 TypedDict 键规则约束(not-subscriptable/invalid-argument-type)。贯穿三者的共同原则是:删除会使赋值窄化失效,但不会把"删除后访问"当作必然错误——前者源于删除对类型窄化约束的撤销,后者源于 ty 对运行时可重新定义场景的保守处理。理解这些规则,即可准确预判 ty 对任意del代码的诊断结果,并能在阅读或扩展 del.md 这类 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 7:39:18

考试信息报名系统实战:SpringBoot+Vue+MySQL全链路开发与并发控制

/* 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 7:38:43

如何理解 Kotlin inline 函数在 JVM 与 klib 后端内联语义的差异?

如何理解 Kotlin inline 函数在 JVM 与 klib 后端内联语义的差异&#xff1f; 【免费下载链接】kotlin The Kotlin Programming Language. 项目地址: https://gitcode.com/GitHub_Trending/ko/kotlin 当你维护一个包含 inline 函数的库&#xff0c;并且依赖库升级后该函…

作者头像 李华
网站建设 2026/9/10 7:37:08

Multica 自建部署怎么开启 Prometheus 指标并保护 /metrics 端点

Multica 自建部署怎么开启 Prometheus 指标并保护 /metrics 端点 【免费下载链接】multica Make humans and AI agents work as one team — open-source and self-hostable. 项目地址: https://gitcode.com/GitHub_Trending/mu/multica 在自建 Multica 时&#xff0c;默…

作者头像 李华
网站建设 2026/9/10 7:36:42

车载智能互联盒子怎么选?从原理到实操的避坑指南

/* 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 7:36:17

Humanizer技能详解:去除AI味,让文字更有温度

做内容这些年&#xff0c;我越来越觉得“humanizer”不是一个软件的名字&#xff0c;而是一套基本功。最近这个词又上了热搜&#xff0c;连带“humanizer skill”一起被大量讨论&#xff0c;很多人以为它是什么黑科技&#xff0c;其实拆开来看&#xff0c;就是把人机感过重的文…

作者头像 李华