Python|四年完成400万行Python代码检查,甚至顺手写了个编译器( 二 )

  • 即使是在大型项目中, mypy 也能够在几分之一秒内完成完整的类型检查 。 运行测试通常需要几十秒或者几分钟 。 类型检查带来的快速反馈 , 能够帮助开发者更快实现迭代 。 这意味着不需要编写脆弱且难以维护的单元测试 , 用以模拟及修复现有代码以获取快速反馈 。
  • 以 PyCharm 以及 Visual Studio Code 为代表的 IDE 和编辑器可利用类型注释实现代码补全、高亮显示错误并支持更好的定义功能——这里仅列出几项典型的功能性应用 。 对于一部分程序员而言 , 这些功能直接决定着他们的生产效率 。 这类用例不需要独立的类型检查工具 。 当然 , 像 mypy 这样的独立工具仍有助于保证注释与代码之间的同步 。
  • 2、启动迁移:性能成为瓶颈在 Dropbox , 我们成立了一个三人小队 , 从 2015 年底开始研究 mypy 。 成员分别是 Guido、Greg Price 以及 David Fisher 。 从那时起 , 工作开始快速推进 。 首先 , 在 mypy 采用面前的最大障碍就是性能 。 我们一直在将其运行在 CPython 解释器上 , 这对于 mypy 这样的工具来说速度有点不够用 。 (作为包含 JIT 编译器的 Python 替代性方案 , PyPy 在这方面也帮不上什么忙 。 )
    幸运的是 , 我们实现了一系列算法层面的改进 。 我们采用的第一项加速措施就是增量检查 。 其背后的思路非常简单:如果模块的所有依赖关系都与 mypy 运行前的状态毫无区别 , 那我们完全可以使用前一次运行的缓存数据获取依赖关系 , 意味着只需要类型检查修改了的文件及其依赖关系 。 mypy 则在此基础上更进一步:如果模块的外部接口没有改变 , mypy 甚至不需要重新检查导入该模块的其它模块 。
    在对现有代码进行批量注释时 , 增量检查确实非常有用 , 因为其中往往涉及 mypy 的大量迭代运行 , 用以处理陆续插入且逐渐细化的类型 。 最初的 mypy 运行仍然相当缓慢 , 这是因为它需要处理大量依赖项 。 为此 , 我们实现了远程缓存 。 如果 mypy 检测到本地缓存可能已经过期 , mypy 将从集中存储库下载整个代码库的最新缓存快照 。 在此之后 , 它会以下载到的缓存为基础执行增量构建 。 这又进一步提高了性能表现 。
    到 2016 年底 , Dropbox 公司已经有大约 42 万行 Python 完成了类型注释 。 很多用户都热衷于类型检查 , 而 mypy 的使用则在 Dropbox 各团队之间迅速传播 。
    情况看起来相当不错 , 但距离真正的成功还有很长的路 。 我们开始定期进行内部用户调查 , 借以找出痛点 , 并确定需要优先考虑的工作(这种习惯直到今天也一直被保持下来) 。 其中 , 有两项请求始终排名最高:更大的类型检查覆盖范围以及更快的 mypy 运行速度 。 很明显 , 我们的性能与采用提升工作还没有全部完成 。 为此 , 我们还得在这两项任务上再多下点力气 。
    Python|四年完成400万行Python代码检查,甚至顺手写了个编译器文章插图
    性能提升方法一:使用 mypy 守护进程
    增量构建虽然提升了 mypy 的速度 , 但仍然没有达到顶峰 。 大量增量运行可能需要一分钟的处理时长 。 对于任何面对大型 Python 代码库的用户来讲 , 其中的原因相信并不难理解:循环导入 。
    我们拥有数百个模块 , 模块相互间接导入 。 如果导入周期的任何文件发生变更 , 那么 mypy 就必须处理周期中的所有文件 , 同时还得处理在此周期内导入该模块的所有其它模块 。 其中最臭名昭著的循环就是“纠结(tangle)” , 它给 Dropbox 带来了很大麻烦 。 其中一度包含有数百个模块 , 众多测试级乃至产品级功能都要或直接或间接地将其导入 。
    我们一直在考虑打理这种纠结无比的依赖关系 , 但却始终没有合适的方法着手进行 。 毕竟我们不熟悉的代码太多了 。 因此 , 我们想出了另一个办法——即使存在这种“纠结” , 我们同样可以提升 mypy 速度 。 答案就是 , 使用 mypy 守护进程 。 守护进程是一项服务器进程 , 负责执行两项非常重要的工作 。
    首先 , 它将关于整体代码库的信息保存在内存中 , 这样每次 mypy 运行就不再需要加载数千条与所导入依赖项相对应的缓存数据 。 其次 , 它会跟踪函数与其构造之间的细粒度依赖关系 。 例如 , 如果函数 foo 调用函数 bar , 那么就存在一项从 bar 到 foo 的依赖关系 。 当文件发生变更时 , 守护程序会首先单独处理已经变更的文件;接下来 , 它会查找该文件中包含的外部可见变更 , 例如变更的函数签名 。 守护程序所采用的细粒度依赖项管理机制 , 能够确保只重新检查实际变更的那些函数——换言之 , 只检查极少数函数 。