对 4 万行 Python 代码进行类型检查的路径。 第1部分

今天,我们提请您注意有关 Dropbox 如何处理 Python 代码类型控制的材料翻译的第一部分。

对 4 万行 Python 代码进行类型检查的路径。 第1部分

Dropbox 用 Python 编写了很多内容。 这是一种我们使用极其广泛的语言,无论是用于后端服务还是桌面客户端应用程序。 我们也经常使用 Go、TypeScript 和 Rust,但 Python 是我们的主要语言。 考虑到我们的规模,我们正在谈论数百万行Python代码,事实证明,此类代码的动态类型不必要地使其理解变得复杂,并开始严重影响劳动生产率。 为了缓解这个问题,我们已经开始逐渐将代码转换为使用 mypy 进行静态类型检查。 这可能是最流行的 Python 独立类型检查系统。 Mypy 是一个开源项目,其主要开发人员在 Dropbox 工作。

Dropbox 是首批在 Python 代码中实现如此规模的静态类型检查的公司之一。 如今,Mypy 已在数千个项目中使用。 正如他们所说,这个工具无数次“在战斗中经过考验”。 我们已经走了很长的路才到达现在的位置。 一路走来,也有很多不成功的尝试和失败的尝试。 这篇文章介绍了 Python 中静态类型检查的历史,从作为我的研究项目的一部分的艰难开始,到现在类型检查和类型提示对于无数使用 Python 编写的开发人员来说已经变得司空见惯。 这些机制现在得到许多工具的支持,例如 IDE 和代码分析器。

阅读第二部分

为什么需要类型检查?

如果您曾经使用过动态类型的 Python,您可能会对为什么最近静态类型和 mypy 如此大惊小怪感到困惑。 或者也许您喜欢 Python 正是因为它的动态类型,而正在发生的事情只是让您感到不安。 静态类型价值的关键在于解决方案的规模:您的项目越大,您就越倾向于静态类型,最终,您就越需要它。

假设某个项目已经达到了几万行的规模,结果发现有几个程序员正在开发它。 看看类似的项目,根据我们的经验,我们可以说理解其代码将是保持开发人员生产力的关键。 如果没有类型注释,则可能很难弄清楚要传递给函数的参数是什么,或者函数可以返回什么类型。 以下是不使用类型注释通常很难回答的典型问题:

  • 这个函数可以返回吗 None?
  • 这个论点应该是什么? items?
  • 什么是属性类型 id: int 是吗, str,或者也许是一些自定义类型?
  • 这个参数应该是一个列表吗? 是否可以将元组传递给它?

如果您查看以下带类型注释的代码片段并尝试回答类似的问题,就会发现这是最简单的任务:

class Resource:
    id: bytes
    ...
    def read_metadata(self, 
                      items: Sequence[str]) -> Dict[str, MetadataItem]:
        ...

  • read_metadata 不返回 None,因为返回类型不是 Optional[…].
  • 争论 items 是一系列行。 不能随机迭代。
  • 属性 id 是一串字节。

在理想的世界中,人们会期望所有这些微妙之处都将在内置文档(文档字符串)中进行描述。 但经验提供了很多例子,表明这样的文档通常不会在您必须使用的代码中被观察到。 即使代码中存在此类文档,也不能指望其绝对正确。 该文档可能含糊、不准确并且容易产生误解。 在大型团队或大型项目中,这个问题可能变得极其严重。

虽然 Python 在项目的早期或中期阶段表现出色,但在某些时候,使用 Python 的成功项目和公司可能会面临一个至关重要的问题:“我们应该用静态类型语言重写所有内容吗?”。

像 mypy 这样的类型检查系统通过为开发人员提供描述类型的形式化语言并检查类型声明是否与程序实现相匹配(以及可选地检查它们的存在)来解决上述问题。 一般来说,我们可以说这些系统为我们提供了诸如仔细检查的文档之类的东西。

使用此类系统还有其他优点,而且它们已经非常重要:

  • 类型检查系统可以检测到一些小的(而且不是那么小的)错误。 一个典型的例子是当他们忘记处理一个值时 None 或其他一些特殊情况。
  • 代码重构大大简化,因为类型检查系统通常非常准确地了解需要更改的代码。 同时,我们不需要希望通过测试获得 100% 的代码覆盖率,无论如何,这通常是不可行的。 我们不需要深入研究堆栈跟踪的深处来找出问题的原因。
  • 即使在大型项目中,mypy 通常也可以在不到一秒的时间内完成完整的类型检查。 而测试的执行通常需要几十秒甚至几分钟的时间。 类型检查系统为程序员提供即时反馈,使他能够更快地完成工作。 他不再需要编写脆弱且难以维护的单元测试,用模拟和补丁替换真实实体,只是为了更快地获得代码测试结果。

PyCharm 或 Visual Studio Code 等 IDE 和编辑器利用类型注释的强大功能为开发人员提供代码补全、错误突出显示以及对常用语言结构的支持。 这些只是打字的一些好处。 对于一些程序员来说,所有这些都是支持打字的主要论据。 这是实施后立即受益的事情。 此类型用例不需要单独的类型检查系统(例如 mypy),但应该注意的是,mypy 有助于保持类型注释与代码一致。

mypy背景

mypy 的历史始于英国剑桥,几年前我加入 Dropbox。 作为我博士研究的一部分,我参与了静态类型和动态语言的统一。 我受到了 Jeremy Siek 和 Walid Taha 撰写的关于增量打字的文章以及 Typed Racket 项目的启发。 我试图找到在各种项目中使用相同编程语言的方法 - 从小型脚本到由数百万行组成的代码库。 与此同时,我想确保在任何规模的项目中,都不必做出太大的妥协。 所有这一切的一个重要部分是逐渐从无类型原型项目转变为经过全面测试的静态类型成品的想法。 如今,这些想法在很大程度上被认为是理所当然的,但在 2010 年,这是一个仍在积极探索的问题。

我最初在类型检查方面的工作并不是针对 Python 的。 相反,我使用了一种小型的“自制”语言 亚洛。 这是一个示例,可以让您了解我们正在讨论的内容(此处类型注释是可选的):

def Fib(n as Int) as Int
  if n <= 1
    return n
  else
    return Fib(n - 1) + Fib(n - 2)
  end
end

使用简化的专有语言是科学研究中常用的方法。 之所以如此,不仅是因为这可以让你快速进行实验,而且还因为与研究无关的东西很容易被忽略。 现实世界的编程语言往往是具有复杂实现的大规模现象,这会减慢实验速度。 然而,任何基于简化语言的结果看起来都有点可疑,因为在获得这些结果时,研究人员可能牺牲了对语言实际使用重要的考虑。

我的 Alore 类型检查器看起来非常有前途,但我想通过试验真实代码来测试它,你可能会说,这些代码不是用 Alore 编写的。 对我来说幸运的是,Alore 语言很大程度上基于与 Python 相同的思想。 更改类型检查器非常容易,以便它可以与 Python 的语法和语义配合使用。 这使我们能够尝试在开源 Python 代码中进行类型检查。 此外,我还编写了一个转译器,将用 Alore 编写的代码转换为 Python 代码,并用它来翻译我的类型检查器代码。 现在我有了一个用 Python 编写的类型检查系统,它支持 Python 的一个子集,即某种语言! (某些对 Alore 有意义的架构决策不太适合 Python,这一点在 mypy 代码库的某些部分中仍然很明显。)

事实上,我的类型系统支持的语言此时还不能完全称为 Python:由于 Python 3 类型注释语法的一些限制,它是 Python 的一个变体。

它看起来像是 Java 和 Python 的混合体:

int fib(int n):
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

我当时的想法之一是使用类型注释来通过将这种 Python 编译为 C 或 JVM 字节码来提高性能。 我已经到了编写编译器原型的阶段,但我放弃了这个想法,因为类型检查本身看起来非常有用。

我最终在圣克拉拉举行的 PyCon 2013 上展示了我的项目。 我还与吉多·范罗苏姆(Guido van Rossum)讨论过这个问题,他是仁慈的 Python 终身独裁者。 他说服我放弃自己的语法并坚持使用标准的 Python 3 语法。Python 3 支持函数注释,因此我的示例可以如下所示重写,从而得到一个正常的 Python 程序:

def fib(n: int) -> int:
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

我必须做出一些妥协(首先,我想指出,正是出于这个原因,我发明了自己的语法)。 特别是,当时该语言的最新版本Python 3.3不支持变量注释。 我通过电子邮件与 Guido 讨论了此类注释的语法设计的各种可能性。 我们决定对变量使用类型注释。 这达到了预期的目的,但有点麻烦(Python 3.6 给了我们更好的语法):

products = []  # type: List[str]  # Eww

类型注释在支持 Python 2 方面也派上用场,Python XNUMX 没有对类型注释的内置支持:

f fib(n):
    # type: (int) -> int
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

事实证明,这些(和其他)权衡并不重要 - 静态类型的好处意味着用户很快就会忘记不太完美的语法。 由于经过类型检查的Python代码中没有使用特殊的语法结构,现有的Python工具和代码处理流程可以继续正常工作,使开发人员更容易学习新工具。

在我完成研究生论文后,Guido 还说服我加入 Dropbox。 这是 mypy 故事中最有趣的部分开始的地方。

待续...

亲爱的读者! 如果您使用 Python,请告诉我们您用这种语言开发的项目的规模。

对 4 万行 Python 代码进行类型检查的路径。 第1部分
对 4 万行 Python 代码进行类型检查的路径。 第1部分

来源: habr.com

添加评论