十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Pyrefly 惰性类型检查探秘:从「继承属性访问」的 demand tree 看按需计算架构

Pyrefly 惰性类型检查探秘:从「继承属性访问」的 demand tree 看按需计算架构 Pyrefly 惰性类型检查探秘从「继承属性访问」的 demand tree 看按需计算架构【免费下载链接】pyreflyA fast type checker and language server for Python项目地址: https://gitcode.com/GitHub_Trending/py/pyrefly导读PyreflyA fast type checker and language server for Python在类型检查上采用了典型的按需demand-driven / lazy计算架构只有被真正需要的模块、符号和类型信息才会被解析与求解。本文以仓库中pyrefly/test_laziness/test_attribute_inherited.md这一条惰性快照测试为核心案例逐行拆解当程序访问一个从父类继承而来的属性时Pyrefly 究竟按需计算了哪些内容、哪些计算是必要的、哪些又是可以继续优化的多余开销。读完之后你将掌握 Pyrefly 五阶段检查流水线Load → Ast → Exports → Answers → Solutions的运作机制学会阅读 demand tree 快照并理解属性查找、MRO 计算等核心路径的底层实现。什么是 Pyrefly 的惰性Laziness机制Pyrefly 不会从头到尾把整个项目每个模块都完整检查一遍。相反它的每个模块都被拆解为一条渐进式流水线只有当前端调用方真正提出需求时后端被依赖模块才会推进到更深的阶段。这条流水线定义在 pyrefly/lib/state/steps.rs 中由Step枚举表示共五个阶段阶段说明Load从磁盘或内存读取源文件内容Ast将源码解析为 AST 与 tokenExports从 AST 构建模块的导出符号表ExportsAnswers对模块内所有绑定绑定表、函数签名做求解得到类型答案AnswersSolutions最终求解——检查错误、解析表达式类型得到完整检查结果Solutions从StepsMut的实现可以看到每个步骤的产物都存放在锁无关的ArcSwapOption槽位中steps.rs并通过AtomicStep记录当前已完成的最大步骤读者只要看到current_step X就能保证第 X 步的数据已经就绪steps.rs。这意味着一个只被import但从未使用的模块可能永远停在Exports阶段一个从未被触碰的依赖模块甚至可以是Nothing一步都不算只有真正被demand需求触及的键Key才会被求解并缓存进计算单元Calculation cell。这套机制的目的非常明确在增量场景尤其是 LSP 编辑器内里把无关模块的计算量压到最低。而验证这套机制是否真的按需的唯一手段就是记录每一次跨模块需求demand形成一棵可读的 demand tree——这正是test_laziness/目录下这批 Markdown 快照测试的职责。测试用例跨三个模块的继承属性访问test_attribute_inherited.md构造了一个最小但非常典型的继承场景涉及三个模块源码见 pyrefly/test_laziness/test_attribute_inherited.mda.py被检查的入口模块from b import Child c Child() x c.base_attrb.py定义Childfrom c import Base class Child(Base): child_attr: str helloc.py定义Base被继承的属性真正所在处class Base: base_attr: int 1访问c.base_attr时base_attr定义在Base模块c中被Child模块b继承。要给出x的类型Pyrefly 必须沿着a → b → c的依赖链逐层解析。这个用例的精妙之处在于一条属性访问语句几乎能覆盖 Pyrefly 惰性检查中所有典型的类相关需求键Key。逐步解读 demand tree哪些需求是必要的a.py检查完成后的需求树快照见 test_attribute_inherited.md如下我们逐条分析a: Solutions b: Answers c: Answers首部三行是每个模块最终停靠的阶段入口a走完了全部流水线Solutions而b、c都只推进到Answers——它们没有被完整检查只是提供了足够的类型信息。这本身就是惰性机制的体现。需求树正文注释为本文添加(66 builtin demands hidden) ← 内建模块builtins/typing的 66 条需求被聚合隐藏 # 解析导入a 需要从 b 拿到 Child 符号 a - b::KeyExport(Name(Child)) # 解析 import得到 Child 类 # 计算 Child 的类元数据含 4 次对 c 的元数据级联 a - b::KeyClassMetadata(ClassDefIndex(0)) b - c::KeyExport(Name(Base)) # 计算 MRO 需要先知道基类 Base b - c::KeyClassMetadata(ClassDefIndex(0)) ×4 # 校验类头关键字、__init_subclass__ 等 # 抽象类检查文档标注对属性访问是多余的 a - b::KeyAbstractClassCheck(ClassDefIndex(0)) b - c::KeyAbstractClassCheck(ClassDefIndex(0)) # MRO 计算为了遍历祖先必须解析 Base a - b::KeyClassMro(ClassDefIndex(0)) b - c::KeyClassMetadata(ClassDefIndex(0)) ×2 b - c::KeyClassBaseType(ClassDefIndex(0)) b - c::KeyClassMro(ClassDefIndex(0)) # 合成字段检查MRO 遍历需要检查每个祖先的合成字段 a - b::KeyClassSynthesizedFields(ClassDefIndex(0)) a - c::KeyClassSynthesizedFields(ClassDefIndex(0)) # 真正的属性解析含对 int 的惰性内建查找 a - b::KeyClassMro(ClassDefIndex(0)) # 再次计算/读取已缓存的 MRO a - c::KeyClassField(ClassDefIndex(0), Name(base_attr)) # 关键一步文档将其中的需求分为两类必要的需求All necessary demandsa - b::KeyExport(Child)—— 解析 import拿到Child类本身a - b::KeyClassMetadata(0)—— 计算Child的元数据以确定其基类MRO 的前提而计算元数据时会校验类头关键字是否与继承的__init_subclass__兼容这需要遍历Child的基类从而再次重新需求Base的已缓存的c::KeyClassMetadata——注意b - c::KeyClassMetadata出现 4 次但因为有计算单元缓存重复需求只是哈希查找 Arc克隆成本可忽略这点在 OPPORTUNITIES.md 中有明确说明b - c::KeyExport(Base)与b - c::KeyClassMetadata(0)—— 解析Child的 MRO 必须先知道Basea - b::KeyClassMro(0)—— 计算 MRO 以便遍历祖先a - c::KeyClassField(0, base_attr)——最终一击在Base上真正解析出base_attr的类型其子需求中还包括对int的惰性内建查找a - b/c::KeyClassSynthesizedFields(0)—— MRO 遍历时每个祖先都要检查合成字段如__slots__、dataclass 生成的字段等。多余的Superfluous需求a - b::KeyAbstractClassCheck(0)—— 抽象类检查只在实例化时才相关纯属性访问根本不需要它。深入源码属性查找为什么要遍历整个 MRO需求树中KeyClassMro、KeyClassSynthesizedFields反复出现根因在于属性解析的实现方式。get_field_from_mro是属性查找的核心pyrefly/lib/alt/class/class_field.rsfn get_field_from_mros( s self, cls: Class, name: Name, get_field: impl Fn(Class, Name) - Options ClassField, ) - OptionWithDefiningClassCows, ClassField { get_field(cls, name) // 先查类自身 .map(|field| WithDefiningClass { value: Cow::Borrowed(field), defining_class: cls.dupe() }) .or_else(|| { // 自身没有才去祖先里找 self.get_field_from_ancestors( cls, self.get_mro_for_class(cls).ancestors(self.stdlib), name, get_field, ) }) }get_class_member_implclass_field.rs在get_field_from_mro之上把当前类字段含合成字段作为查询函数传入。而合成字段的获取本身又会需求KeyClassSynthesizedFieldspub(crate) fn get_synthesized_field_from_current_class_only(...) - OptionClassField { Some(self.get_from_class(cls, KeyClassSynthesizedFields(cls.index()))?.get(name)?...) }需求树中a - b::KeyClassMro(0)出现两次也由此解释第一次来自 MRO 本身的计算get_mro_for_class第二次来自get_field_from_mro中通过ancestors遍历祖先的需求。而遍历祖先的循环位于 pyrefly/lib/alt/attr.rsfor ancestor in mro.ancestors_no_object()它会逐个访问 MRO 中的每一个祖先——即使属性在第一个类上就已命中也不会提前退出。这正是对比测试test_attribute_on_class_itself.mdpyrefly/test_laziness/test_attribute_on_class_itself.md要揭示的问题当base_attr换成定义在Child自身的child_attr时需求树与本测试几乎完全相同Base所在的模块c依旧被解析了 7 个键——而按理想情况这些本应全部是多余的。从单条快照到整体优化机会test_attribute_inherited.md展示的并非孤例而是 Pyrefly 惰性机制中类层级相关需求的一个缩影。仓库中的 pyrefly/test_laziness/OPPORTUNITIES.md 汇总了多份 demand tree 的统计结论与本用例直接相关的有MRO 无早退遍历是大头类相关键KeyClassSynthesizedFieldsKeyClassMetadataKeyClassMroKeyClassField合计约占全部跨模块需求的 64%。其中KeyClassSynthesizedFields约 24%占 Answer 需求 36%、KeyClassMetadata约 23%、KeyClassMro约 9%、KeyClassField约 8%。深继承层级越深开销越明显OPPORTUNITIES.md抽象类检查的定位每次Foo()实例化都会触发KeyAbstractClassCheck它递归枚举所有父类的抽象方法。本测试证明它在属性访问路径上纯属多余OPPORTUNITIES.md重复需求不可怕同一个键被多次需求是正常现象——计算单元会缓存结果重复只是哈希查找 Arc克隆。只有针对真正不必要的键的需求才是优化目标OPPORTUNITIES.md。文中提到的理想优化方向先查类自身、命中即返回仅在未命中时才物化 MRO 迭代器等就是这些测试驱动下的待办清单。如何运行与复现这套验证惰性快照测试的运作方式test_laziness/目录下的每个.md文件都是一条快照测试文档中嵌入 Python 文件源码和## Check目标以及expected代码块中的期望输出模块最终阶段 demand tree。测试运行器位于 pyrefly/test_laziness/mod.rsparse_test解析 Markdown提取(模块名, 源码)列表与检查目标mod.rsrun_test用MapDatabase将源码放入内存强制单线程ThreadCount::NumThreads(1)执行以保证 demand tree 顺序确定性再挂上TestSubscriber与DemandCollector收集每一步信息mod.rs输出按规则渲染检查目标模块标为Solutions优先排序其余模块按字母序builtins/typing的跨模块需求被聚合为(N builtin demands hidden)一行避免淹没关键信息mod.rsrun_laziness_test将实际输出与期望快照比对不一致时给出 unified diff并可一键重录快照mod.rs。若快照失配运行器会打印类似如下的重录命令UPDATE_SNAPSHOTS1 cargo test test_attribute_inherited -- --test-threads1在 buck 环境下对应buck test pyrefly:pyrefly_laziness_tests -- --env UPDATE_SNAPSHOTS1 -r test_attribute_inherited见 mod.rs。设置UPDATE_SNAPSHOTS1后差异会就地写回源.md文件并让测试失败以便开发者通过 diff 审查改动——这正是快照即规格的维护方式。在真实项目上观察 demand tree同样的需求收集机制也暴露给了 CLIpyrefly check --report-demand-tree out.json file。该参数在 pyrefly/lib/commands/check.rs 中定义检查完成后会从事务中取走全部需求根节点连同每个模块的最终阶段Step::label缺失则为Nothing一起序列化为 JSON 写出check.rs。OPPORTUNITIES.md中的百分比统计正是用它对真实代码库采样聚合而来——你完全可以对自家项目跑同样的命令验证哪些模块被推进到了过深的阶段。小结一条属性访问背后的一整套按需机制test_attribute_inherited.md用 20 余行快照回答了一个关键问题Pyrefly 在最普通的继承属性读取上到底做了多少必要工作、多少多余工作答案是解析 import、类元数据、MRO、合成字段、最终字段解析都是必要的而KeyAbstractClassCheck是明确的多余需求。同时b、c两个依赖模块都只停在Answers阶段而不是被完整检查这证明了流水线分级的有效性。对 Pyrefly 的贡献者来说这套测试是一份可执行的需求规格任何改动如果改变了跨模块计算的时机快照测试会立刻报警。对使用者来说它揭示了类型检查器性能优化的真实战场——MRO 遍历、类元数据、合成字段这些看不见的键恰恰是大型代码库检查耗时的最大来源。理解 demand tree就是理解 Pyrefly 性能调优的第一步。延伸阅读惰性测试框架整体实现pyrefly/test_laziness/mod.rs五阶段流水线与 Step 定义pyrefly/lib/state/steps.rs属性查找与 MRO 遍历实现pyrefly/lib/alt/class/class_field.rs、pyrefly/lib/alt/attr.rs惰性优化机会汇总含真实项目统计pyrefly/test_laziness/OPPORTUNITIES.md对照组属性定义在类自身时的需求树pyrefly/test_laziness/test_attribute_on_class_itself.md命令行导出需求树pyrefly/lib/commands/check.rs【免费下载链接】pyreflyA fast type checker and language server for Python项目地址: https://gitcode.com/GitHub_Trending/py/pyrefly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表