类型检查400万行Python代码的方式。 第二部分

今天,我们发布材料翻译的第二部分,内容涉及Dropbox如何组织数百万行Python代码的类型控制。



阅读第一部分

正式类型支持(PEP 484)


在2014年Hack Week期间,我们在Dropbox上对mypy进行了首次严重实验。Hack Week是Dropbox举办的为期一周的活动。 目前,员工可以从事任何工作! Dropbox的一些最著名的技术项目始于类似的活动。 作为该实验的结果,尽管该项目尚未准备好广泛使用,但我们得出的结论是mypy看起来很有希望。

那时,标准化用于Python类型的提示系统的想法浮出水面。 就像我说的那样,从Python 3.0开始,您可以为函数使用类型注释,但是它们只是任意表达式,没有特定的语法和语义。 在程序执行期间,这些注释大部分都被忽略了。 Hack Week之后,我们开始着手标准化语义。 这项工作促成了PEP 484的出现(Guido van Rossum,Lukas Langa和我在此文档上进行了合作)。

我们的动机可以从两个方面来看。 首先,我们希望整个Python生态系统可以采用一种通用的方法来使用类型提示(类型提示是Python中用作“类型注释”的类似术语)。 考虑到可能的风险,这比使用许多相互不兼容的方法更好。 其次,我们想与Python社区的许多成员公开讨论类型注释机制。 在某种程度上,这种愿望是由这样的事实所决定的:我们不想在广大Python程序员的眼中看起来像该语言的基本思想的“叛教者”。 它是一种动态键入的语言,称为“鸭子键入”。 在社区中,从一开始,就对静态类型化的想法有点怀疑。 但是这种态度最终减弱了-在明确计划不强制使用静态类型之后(人们意识到它确实有用)之后。

类型提示的最终语法与当时支持的一种mypy非常相似。 PEP 484于2015年随Python 3.5发布。 Python不再是仅支持动态类型化的语言。 我喜欢将此事件视为Python历史上的重要里程碑。

开始迁移


2015年底,在Dropbox中创建了一个由3个人组成的团队来研究mypy。 其中包括Guido van Rossum,Greg Price和David Fisher。 从那一刻起,情况开始迅速发展。 狂犬病成长的第一个障碍是表现。 正如我上面已经暗示的那样,在项目开发的早期,我正在考虑将mypy的实现转换为C,但是到目前为止,这种想法已从清单中删除。 我们一直坚持使用CPython解释器来启动系统这一事实,对于像mypy这样的工具来说这还不够快。 (PyPy项目是带有JIT编译器的Python的另一种实现,也没有帮助我们。)

幸运的是,这里有一些算法上的改进对我们有所帮助。 第一个强大的“加速器”是增量验证的实施。 改进的想法很简单:如果自上次启动mypy以来模块的所有依赖项都没有改变,那么我们就可以在处理依赖项时使用上一个会话期间缓存的数据。 我们要做的就是在修改后的文件以及依赖它们的文件中进行类型检查。 Mypy走得更远:如果模块的外部接口没有更改-mypy认为不需要再次检查导入该模块的其他模块。

增量验证极大地帮助我们注释了大量现有代码。 事实是,随着批注逐渐添加到代码中并逐渐得到改进,该过程通常涉及mypy的许多迭代运行。 mypy的首次启动仍然非常缓慢,因为运行它时,您必须检查很多依赖项。 然后,为了改善这种情况,我们实现了远程缓存机制。 如果mypy检测到本地缓存可能已过期,则会从集中式存储库下载整个代码库的当前缓存快照。 然后,他使用此快照执行增量检查。 这是迈出的又一大步,使我们朝着提高Mypy生产力迈进。

这是Dropbox类型检查系统快速自然引入的时期。 到2016年底,我们已经有大约420,000行带有类型注释的Python代码。 许多用户都热衷于类型检查。 Dropbox mypy已被越来越多的开发团队使用。

当时一切看起来不错,但是我们还有很多事情要做。 我们开始进行定期的内部用户调查,以便确定项目的问题区域并了解首先需要解决的问题(今天的公司中已采用这种做法)。 显而易见,最重要的是两项任务。 第一个-您需要更多类型的代码覆盖率,第二个-mypy必须更快地工作。 显然,我们在加速mypy及其在公司项目中的实施工作还远远没有完成。 我们充分意识到这两项任务的重要性,因此采取了解决方案。

性能更高!


增量检查加速了mypy,但是此工具仍然不够快。 许多增量检查持续了大约一分钟。 其原因是周期性进口。 对于使用Python编写的大型代码库工作的人,这可能不会感到惊讶。 我们有成百上千个模块,每个模块都间接导入所有其他模块。 如果发现导入周期中的任何文件被修改,则mypy必须处理该周期中包含的所有文件,并且通常还必须处理从该周期中导入模块的所有模块。 这样一个周期就是臭名昭著的“依赖关系缠结”,这在Dropbox中造成了很多麻烦。 一旦该结构包含数百个模块,就直接或间接导入了许多测试,同时又将其用于生产代码中。

我们考虑了“解散”循环依赖关系的可能性,但是我们没有资源来做到这一点。 有太多我们不熟悉的代码。 结果,我们采取了另一种方法。 我们决定即使存在“依赖球”,也要使mypy快速工作。 我们使用mypy守护程序完成了此任务。 守护程序是一个服务器进程,它实现了两个有趣的功能。 首先,它将有关整个代码库的信息保留在内存中。 这意味着您每次运行mypy时,都不必下载与成千上万个依赖项相关的缓存数据。 其次,他在小型结构单元的层次上仔细分析了职能与其他实体之间的关系。 例如,如果函数foo调用函数bar ,则foobar有依赖性。 更改文件后,守护程序首先单独处理仅更改后的文件。 然后,他查看从外部可见的对该文件的更改,例如更改的功能签名。 守护程序仅使用详细的导入信息来仔细检查真正使用更改后的功能的那些功能。 通常,使用这种方法,几乎​​不需要检查任何功能。

实现所有这些都不容易,因为mypy的原始实现主要集中于一次处理一个文件。 我们必须处理许多临界情况,在代码中发生某些更改的情况下,需要反复检查这些情况。 例如,当将新的基类分配给一个类时,就会发生这种情况。 完成所需的操作后,我们可以将大多数增量检查的执行时间减少到几秒钟。 在我们看来,这是一个伟大的胜利。

性能更高!


上面我已经描述了mypy守护程序与远程缓存(如上所述)一起几乎完全解决了程序员经常运行类型检查(对少量文件进行更改)时出现的问题。 但是,在使用性能最差的变体中,系统性能仍然远非最佳。 干净启动mypy可能需要15分钟以上。 这远远超出了我们的期望。 每周,情况都会变得更糟,因为程序员继续编写新代码并在现有代码中添加注释。 我们的用户仍然渴望获得更高的性能,但是我们很高兴能与他们见面。

我们决定返回关于mypy的最早想法之一。 即,将Python代码转换为C代码。 Cython(这是一个允许您将Python代码转换为C代码的系统)进行的实验没有给我们带来任何明显的加速,因此我们决定恢复编写自己的编译器的想法。 由于mypy代码库(用Python编写)已经包含所有必需的类型注释,因此尝试使用这些注释来加快系统速度似乎是值得的。 我迅速创建了一个原型来测试这个想法。 他在各种微型基准上显示出生产率提高了10倍以上。 我们的想法是使用Cython工具将Python模块编译为C模块,并将类型注释转换为在运行时执行的类型检查(通常,类型注释在运行时会被忽略,并且仅由类型检查系统使用) 实际上,我们计划将mypy实现从Python转换为静态创建的语言,该语言看起来(在大多数情况下可以正常工作)与Python完全一样。 (这种跨语言迁移已成为mypy项目的传统。mypy的最初实现是用Alore编写的,然后是Java和Python的语法混合)。

专注于CPython扩展API是不丧失项目管理功能的关键。 我们不需要实现虚拟机或mypy所需的任何库。 此外,我们仍然可以使用整个Python生态系统,所有工具(例如pytest)都可以使用。 这意味着我们可以在开发过程中继续使用解释后的Python代码,这将使我们能够继续使用非常快速的方案进行工作,以进行代码更改和测试,而不必等待代码编译。 可以这么说,我们似乎有能力坐在两把椅子上,所以我们喜欢它。

我们将编译器命名为mypyc(因为它使用mypy作为类型分析的前端),事实证明这是一个非常成功的项目。 总而言之,在不进行缓存的情况下,我们将mypy的频繁运行速度提高了约4倍。 mypyc项目核心的开发大约花了4个日历月,这是由一个由Michael Sullivan,Ivan Levkivsky,Hugh Han和我组成的小组组成的。 这项工作的工作量远远不如重写mypy所需的工作(例如,在C ++或Go中)。 而且,与对其他语言进行重写相比,我们对项目所做的更改要少得多。 我们还希望我们可以将mypyc提升到其他Dropbox程序员可以使用它来编译和加速其代码的水平。

为了达到这种性能水平,我们必须应用一些有趣的工程解决方案。 因此,编译器可以通过使用快速的低级C构造来加快许多操作,例如,对已编译函数的调用会转换为对C函数的调用。 这样的调用比调用解释函数要快得多。 诸如字典搜索之类的某些操作仍然归结为使用来自CPython的常规C-API调用,事实证明,这些操作在编译后只会快一点。 我们能够摆脱由解释产生的系统上的额外负载,但是在这种情况下,这在性能方面仅获得了很小的收益。

为了确定最常见的“慢速”操作,我们执行了代码分析。 借助获得的数据,我们尝试调整mypyc以便为此类操作生成更快的C代码,或者使用更快的操作重写相应的Python代码(有时我们对此没有足够简单的解决方案或其他问题)。 与在编译器中自动实现相同的转换相比,重写Python代码通常被证明是解决该问题的简便方法。 从长远来看,我们希望使许多这样的转换自动化,但是那一刻我们的目标是以最小的努力来加快mypy的速度。 我们朝着这个目标迈进了几个角落。

待续...

亲爱的读者们! 当您了解mypy项目的存在时,您对它的印象如何?


Source: https://habr.com/ru/post/zh-CN468235/


All Articles