ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ty 如何推断 dataclass 字段类型:基于 mdtest 测试用例的 dataclasses.field 语义全解

ty 如何推断 dataclass 字段类型:基于 mdtest 测试用例的 dataclasses.field 语义全解 ty 如何推断 dataclass 字段类型基于 mdtest 测试用例的 dataclasses.field 语义全解【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff本文以 tyRuff 仓库中的 Python 类型检查器的一组 mdtest 测试文件为骨架完整拆解 ty 对dataclasses字段的静态分析行为field()字段说明符如何改写__init__签名、default/default_factory/init/kw_only等参数对类型推断的影响、KW_ONLY哨兵与描述符协议的处理细节以及dataclass_transform自定义字段说明符的识别边界。读完本文你可以对照 ty 的源码FieldKind::Dataclass元数据结构与字段说明符绑定逻辑理解每一条类型检查结论的底层实现依据。一、测试载体mdtest 与 dataclasses 测试集该文档位于 crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md是 ty 类型推断引擎ty_python_semanticcrate的 in-doc 测试文件。这类测试用真实 Python 代码块加行内注解表达断言reveal_type(x) # revealed: 类型断言某表达式推断出的类型# error: [rule-id] 消息断言某行应触发指定诊断独立的toml块声明该文件的检查环境例如[environment] python-version 3.12用于测试依赖特定 Python 版本特性的场景kw_only、KW_ONLY。同目录还有覆盖 dataclass 其他侧面的测试文件dataclasses.md基础行为、dataclass_transform.mddataclass_transform协议、post_init.md__post_init__。fields.md聚焦的正是“字段field”这一层默认值、初始化参数、字段说明符元数据的保留与丢弃。二、基本字段default与initFalse文档的第一个用例展示了field()说明符对__init__签名的三种影响from dataclasses import dataclass, field dataclass class Member: name: str role: str field(defaultuser) tag: str | None field(defaultNone, initFalse) # revealed: (self: Member, name: str, role: str user) - None reveal_type(Member.__init__) alice Member(nameAlice, roleadmin) reveal_type(alice.role) # revealed: str alice.role moderator # tag is marked as initFalse, so this is an # error: [unknown-argument] Argument tag does not match any known parameter bob Member(nameBob, tagVIP)三个要点必填字段与可选字段name没有默认值在__init__中是位置参数role因field(defaultuser)获得默认值签名中表现为role: str user。initFalse使字段退出__init__tag虽然声明为str | None但因initFalse完全不出现在构造签名中。任何把它作为关键字传入的调用都会被报[unknown-argument]诊断——这正是文档最后一行Member(nameBob, tagVIP)触发的错误。实例属性类型保持声明类型alice.role推断为str且字段可被重新赋值非frozen场景。从源码结构看这些行为由ty_python_semantic中的字段元数据驱动。crates/ty_python_semantic/src/types/class.rs 定义了FieldKind枚举其中FieldKind::Dataclass变体携带了测试所验证的全部语义FieldKind::Dataclass { /// 该字段的默认值类型 default_ty: OptionTypedb, /// init-only 字段只出现在 __init__ 签名中不是真实可访问字段 init_only: bool, /// 是否出现在 __init__ 签名中 init: bool, /// 是否只能以关键字参数传给 __init__ kw_only: Optionbool, /// 在 __init__ 签名中的参数名如果指定了 alias alias: OptionBoxstr, /// converter 指定时的输入/输出类型 converter: Option(Typedb, Typedb), }字段“是否必填”的判定逻辑同样在 class.rs 的Field::is_required中数据类字段仅当“没有默认值default_ty为空且init为真”时才必填——这与文档中role有默认值和taginitFalse都不进入必填参数的事实一一对应。三、Any注解不丢失字段说明符元数据文档第二节的结论是即便字段显式标注为Any右侧的field(...)调用仍会被识别为字段说明符合成字段类型的依据是右侧表达式而非声明类型from dataclasses import dataclass, field from typing import Any dataclass class AnyFieldSpecifier: proto: Any field(reprFalse) key: str | None reveal_type(AnyFieldSpecifier.__init__) # revealed: (self: AnyFieldSpecifier, proto: Any, key: str | None) - None dataclass class AnyInitFalseField: key: str | None proto: Any field(initFalse) reveal_type(AnyInitFalseField.__init__) # revealed: (self: AnyInitFalseField, key: str | None) - None注意两个__init__的差别AnyFieldSpecifier中proto: Any field(reprFalse)没有默认值参数因此proto仍是必填位置参数类型保持Anyfield说明符返回的默认值类型在此即Any。AnyInitFalseField中proto: Any field(initFalse)因initFalse而完全不出现在构造签名中。从源码结构看这个“以右侧为准”的行为在字段说明符的调用绑定中实现ty 对dataclasses.field及各类第三方字段说明符统一采取“假装返回默认值类型”的策略见下文 第五节 中 bind.rs 的分析因此无论声明类型是否为Any说明符元数据init、kw_only等都从右侧调用中提取。四、带默认值的继承from dataclasses import dataclass class Configuration: ... dataclass(frozenTrue) class SomeClass: config: Configuration | None def foo(self) - int: raise NotImplementedError class SpecificConfiguration(Configuration): x: int 0 dataclass(frozenTrue) class SpecificClass(SomeClass): config: SpecificConfiguration | None None def foo(self) - int: if self.config is None: return SpecificConfiguration().x return self.config.x reveal_type(SpecificClass().config) # revealed: SpecificConfiguration | None dataclass(frozenTrue) class NoDefaultSpecificClass(SomeClass): config: SpecificConfiguration | None reveal_type(NoDefaultSpecificClass(SpecificConfiguration()).config) # revealed: SpecificConfiguration | None该用例验证三件事字段重声明子类SpecificClass把父类字段config从Configuration | None收窄为SpecificConfiguration | None并补上默认值None。实例上config的类型取子类声明即SpecificConfiguration | None。不带默认值的重声明NoDefaultSpecificClass重声明了字段但没有给默认值因此构造时config是必填参数——NoDefaultSpecificClass(SpecificConfiguration())合法。frozenTrue与继承方法重写foo在子类中被具体实现self.config.x的访问类型随声明收窄而收窄。父类字段带默认值、子类字段不带默认值时必填性沿 MRO 重新计算这与第二节提到的is_required判定逻辑一致只看“当前声明有没有默认值 init 是否开启”。五、ty 的字段说明符绑定实现源码级视角上述所有“右侧field(...)被解析为元数据”的行为都汇聚到 ty 的调用绑定器 crates/ty_python_semantic/src/types/call/bind.rs 中对字段说明符函数的特殊处理。关键片段如下function Type::FunctionLiteral(function_type) if dataclass_field_specifiers.contains(function) { // 按名字取关键字参数类型优先从显式参数绑定取 // 再回退到调用点实参兼容 **kwargs 风格的说明符 let get_argument_type |name, fallback_to_default| - OptionTypedb { ... }; let default get_argument_type(default, false); let has_default_value get_argument_type(default_factory, false) .is_some() || get_argument_type(factory, false).is_some() || default.is_some_and(...); let init get_argument_type(init, true); let kw_only get_argument_type(kw_only, true); let alias get_argument_type(alias, true); let validation_alias get_argument_type(validation_alias, true); let converter get_argument_type(converter, true); // ... }其中值得注意的实现细节get_argument_type的双重查找先尝试从参数绑定中取显式形参如dataclasses.field的具名参数再回退到调用点的关键字实参。这一回退使**kwargs风格的第三方说明符如field_specifiers中自定义的说明符也能贡献init、default等元数据。默认值来源的并集default、default_factory、factory三者任一存在即认为字段有默认值default_factory的识别对应 known_instance.rs 中对dataclasses.field()之default_factory参数的专门处理。init缺省为真init.map(|init| !init.bool(db, env).is_always_false()).unwrap_or(true)——不写initFalse就视为参与初始化。kw_only的版本门控标准库说明符要求 Python 3.10 及以上才解析kw_only字面量第三方字段说明符则可以更早支持// 标准库字段说明符要求 Python 3.10 才启用 kw_only // 第三方字段说明符可以更早支持 let kw_only if env.python_version(db) PythonVersion::PY310 || !function_type.is_known(db, KnownFunction::Field) { ... } else { None };这解释了为什么文档中kw_only与KW_ONLY用例都显式声明了[environment] python-version 3.12环境。“假装返回默认值类型”策略注释明确指出dataclasses.field、pydantic、attrs、SQLAlchemy 等库的字段说明符“都会返回字段的默认值类型或Any而非真正的Field实例”ty 顺势把说明符调用的返回类型当作默认值类型使用。这一策略正是第三节Any注解用例成立的前提元数据来自右侧调用默认值类型即调用返回类型。六、描述符类型的字段与默认值当字段的声明类型本身是一个描述符类时实例访问必须走描述符协议且默认值不应把描述符类型泄漏到实例属性类型上from dataclasses import dataclass from typing import Any, Generic, TypeVar, overload T TypeVar(T) class Desc2(Generic[T]): overload def __get__(self, instance: None, owner: Any) - list[T]: ... overload def __get__(self, instance: object, owner: Any) - T: ... def __get__(self, instance: object | None, owner: Any) - list[T] | T: raise NotImplementedError dataclass class DC2: x: Desc2[int] y: Desc2[str] z: Desc2[str] Desc2() dc2 DC2(Desc2(), Desc2(), Desc2()) # On the class, __get__(None, owner) is called, returning list[T]. reveal_type(DC2.z) # revealed: list[str] # On instances, __get__(instance, owner) is called, returning T. # The default value should not cause the declared descriptor type # to leak into the instance attribute type. reveal_type(dc2.z) # revealed: str语义拆解Desc2只有__get__没有__set__属于非数据描述符non-data descriptor。类级访问DC2.z触发__get__(None, owner)重载分支返回list[T]即list[str]。实例访问dc2.z触发__get__(instance, owner)重载分支返回T即str。关键点在于z带有默认值Desc2()。如果 ty 把默认值的类型Desc2[str]实例错误地并入实例属性类型dc2.z就会被推断为包含Desc2[str]的联合。测试断言结果为纯str证明默认值只影响__init__签名不污染描述符协议解析出的实例属性类型。七、default_factoryfrom dataclasses import dataclass, field from datetime import datetime dataclass class Data: content: list[int] field(default_factorylist) timestamp: datetime field(default_factorydatetime.now, initFalse) # revealed: (self: Data, content: list[int] ...) - None reveal_type(Data.__init__) data Data([1, 2, 3]) reveal_type(data.content) # revealed: list[int] reveal_type(data.timestamp) # revealed: datetimedefault_factory与default的差异在这里有两处体现可变默认值的标准写法list这类可变对象不能直接作为default运行时会共享同一实例default_factorylist让每个实例获得独立列表。对类型系统而言两者效果相同字段在__init__中获得默认值标记content: list[int] ...不再是必填参数。default_factory与initFalse组合timestamp用default_factorydatetime.now初始化但不出现在__init__中签名里只有content实例访问data.timestamp仍推断为声明类型datetime。这与第二节源码分析中has_default_value判定default_factory存在即视为有默认值的逻辑一致bind.rs也与 class.rs 中is_required注释“有default或default_factory的字段不是必填”的表述互相印证。八、kw_only仅关键字参数该用例显式声明 Python 3.12 环境kw_only参数是 3.10 引入的特性[environment] python-version 3.12from dataclasses import dataclass, field dataclass class Person: name: str age: int | None field(defaultNone, kw_onlyTrue) role: str field(defaultuser, kw_onlyTrue) # revealed: (self: Person, name: str, *, age: int | None None, role: str user) - None reveal_type(Person.__init__) alice Person(roleadmin, nameAlice) # error: [too-many-positional-arguments] Too many positional arguments: expected 1, got 2 bob Person(Bob, 30)推断出的__init__签名中两个kw_onlyTrue字段被提升到*之后成为仅关键字参数name保持位置参数。因此Person(Bob, 30)尝试以位置方式传age触发[too-many-positional-arguments]诊断——“预期 1 个位置参数实际 2 个”。alice Person(roleadmin, nameAlice)展示了合法调用形态name关键字或位置均可role、age必须走关键字。对应实现即第五节引出的kw_only元数据FieldKind::Dataclass.kw_only: Optionbool直接参与合成__init__签名时参数的“关键字分隔”处理。Option的三态未指定 / 真 / 假允许区分“未写kw_only”和显式kw_onlyFalse并配合继承时的kw_only_default语义父类dataclass(kw_only...)的默认值向下传递标准库场景受 3.10 版本门控。九、KW_ONLY哨兵标记而非真实属性KW_ONLYPython 3.12 引入是类体中的分隔标记不是数据字段。测试分三部分验证9.1 哨兵不可作为属性访问[environment] python-version 3.12from dataclasses import dataclass, KW_ONLY dataclass class DC: sentinel: KW_ONLY name: str dc DC(nameAlice) # error: [unresolved-attribute] dc.sentinel # error: [unresolved-attribute] DC.sentinel无论从实例还是类上访问sentinel都应报[unresolved-attribute]哨兵不是真实属性。9.2 父类真实字段与子类哨兵的交互虽然惯例用_作哨兵名但任意名字都可以。若父类把该名字定义为真实字段父类字段被继承哨兵只影响子类中其后声明的字段from dataclasses import dataclass, KW_ONLY dataclass class Parent: _: int dataclass class Child(Parent): _: KW_ONLY name: str # Parents _: int field is inherited; the sentinel makes name keyword-only. # revealed: (self: Child, _: int, *, name: str) - None reveal_type(Child.__init__) c Child(1, nameAlice) reveal_type(c._) # revealed: int合成签名(self: Child, _: int, *, name: str) - None精确表达了运行期行为_是继承来的位置参数哨兵把name切到关键字一侧。c._可正常访问且类型为int。源码层面的支撑在 class.rsimpldb Fielddb { /// Returns true if this field is a dataclasses.KW_ONLY sentinel. pub(crate) fn is_kw_only_sentinel(self, db: db dyn Db) - bool { self.declared_ty.is_instance_of(db, KnownClass::KwOnly) } }判定标准是“声明类型是否为dataclasses.KW_ONLY的实例”。由于哨兵本身是类体绑定而 dataclass 字段收集阶段要区分“哨兵声明”与“同名的继承字段”Child.__init__中_保留、name关键字化这一结果正是该判定在 MRO 字段收集路径上的表现。十、dataclass_transform的field_specifiers边界最后一个主题考察第三方“dataclass-like”框架通过typing_extensions.dataclass_transform标注的字段说明符识别规则核心规则是field_specifiers缺省为空元组此时不应把任何field式右侧当作说明符即便配置了其他说明符未列名的调用依然是普通默认值。from typing_extensions import dataclass_transform from dataclasses import field, dataclass from typing import Any, TypeVar T TypeVar(T) dataclass_transform() def create_model(*, init: bool True): def deco(cls: type[T]) - type[T]: return cls return deco create_model() class A: name: str field(initFalse) # Without explicit field_specifiers, field(initFalse) is an ordinary default RHS. reveal_type(A.__init__) # revealed: (self: A, name: str ...) - None class OtherFieldInfo: def __init__(self, default: Any None, **kwargs: Any) - None: ... def other_field(default: Any None, **kwargs: Any) - OtherFieldInfo: return OtherFieldInfo(defaultdefault, **kwargs) dataclass_transform(field_specifiers(other_field, OtherFieldInfo)) def create_model_with_other_specifiers(*, init: bool True): def deco(cls: type[T]) - type[T]: return cls return deco create_model_with_other_specifiers() class C: name: str field(initFalse) # Even with other active field_specifiers, an unlisted RHS is an ordinary default value. reveal_type(C.__init__) # revealed: (self: C, name: str ...) - None dataclass class B: name: str field(initFalse) # Regular dataclass should respect field(initFalse) reveal_type(B.__init__) # revealed: (self: B) - None三个装饰类对比类装饰器field(initFalse)的解释__init__结果Adataclass_transform()无field_specifiers普通默认值(self: A, name: str ...) - NoneC声明了field_specifiers(other_field, OtherFieldInfo)仍为普通默认值field未列入(self: C, name: str ...) - NoneB标准dataclass真正的字段说明符(self: B) - None构造调用测试进一步验证了行为差异# These should NOT error because As field(...) call is treated like any other default value A() A(namefoo) C() C(namefoo) # This should error because field(initFalse) is respected for B # error: [unknown-argument] B(namefoo)A()与A(namefoo)都合法说明name对A而言就是个带默认值的普通参数而B(namefoo)必须报错因为标准dataclass正确执行了initFalse语义name不在构造参数中。从源码结构看这正是 bind.rs 中dataclass_field_specifiers.contains(function)这个前置条件的体现只有当前函数属于该类声明的说明符集合标准dataclass场景下天然包含dataclasses.field/Fielddataclass_transform场景下则取field_specifiers元组缺省为空时调用绑定器才会提取default/init/kw_only等元数据否则右侧按普通默认值表达式处理。十一、小结字段类型检查的判定链把 fields.md 的全部用例串起来可以得到 ty 对 dataclass 字段的完整判定链且每一环都有源码落点识别说明符右侧表达式是否属于当前类的字段说明符集合标准dataclass含dataclasses.fielddataclass_transform以field_specifiers为准缺省为空——bind.rs提取元数据从说明符调用中解析default/default_factory/factory、init缺省为真、kw_only标准库受 3.10 版本门控、alias、converter——bind.rs沉淀字段结构写入FieldKind::Dataclassdefault_ty、init、kw_only、alias、converteris_required由“无默认值且 init”决定——class.rs合成__init__签名必填字段为位置参数、有默认值字段带默认值、kw_only字段置于*之后、initFalse字段被剔除未知参数名触发[unknown-argument]、多余位置参数触发[too-many-positional-arguments]属性访问解耦默认值实例属性类型取声明类型描述符字段走__get__协议默认值类型不参与实例属性联合哨兵隔离KW_ONLY声明经is_kw_only_sentinel识别后不产生可访问属性仅改写其后字段的参数位置。对使用者而言这套语义意味着在 ty 下写 dataclassreveal_type(X.__init__)可以完整审计构造签名initFalse、kw_only、default_factory等运行时语义在类型层面与 Python 官方实现保持一致而自研 dataclass-like 框架时通过dataclass_transform(field_specifiers...)显式声明说明符才能让 ty 正确识别initFalse等元数据——否则字段右侧会被当作普通默认值构造签名的必填/可选判定将与运行时行为错位。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表