深入了解Python类型提示

  自从Python的类型提示在2014年发布以来,人们一直致力于将它们引入到他们的代码库中。现在,我大胆地估计大约有20-30%的Python 3代码库使用提示(有时也称为注释)。在过去的一年里,我看到他们出现在越来越多的书籍和教程中。

  实际上,现在我很好奇——如果你经常在python3中进行开发,你是否在你的代码中使用类型注释/提示?

  — Vicki Boykis (@vboykis) May 14, 2019

  下面是使用了类型提示的代码的典型示例。

  类型提示前的代码:

  

  类型提示后的代码:

  

  提示的样板格式通常是:

  

  然而,对于它们是什么仍然有很多让人困惑的地方(甚至它们的名称是什么——它们是提示还是注释?对于本文来说,我将把它们称为提示),以及它们如何有益于你的代码库。

  当我开始调查和权衡类型提示是否适合我使用时,我变得超级困惑。因此,就像我通常对我不理解的事情所做的那样,我决定深入挖掘,并希望这篇文章也将有助于其他人。

  像往常一样,如果你看到某些内容并想进行评论,请随意提交pull请求。

  要理解Python核心开发人员在这里使用类型提示所做的工作,我们从Python中向下几个级别来深入了解,并更好地理解计算机和编程语言的一般工作方式。

  编程语言的核心是使用CPU处理数据,并将输入和输出都存储在内存中的一种方式。

  

  CPU是很蠢的。它可以做非常强大的事情,但它只懂机器语言,而机器语言的核心是电。机器语言的一个表示形式是1和0。

  为了获得那些1和0,我们需要从高级语言转换到低级语言。这就是编译语言和解释语言的用武之地。

  当语言被编译或执行时(python是通过解释器执行的),代码被转换成低级的机器代码,告诉计算机的低级组件,即硬件,要做什么。

  有两种方法可以将代码转换成机器可读的代码: 你可以构建一个二进制文件并让编译器翻译它(c++、Go、Rust等),或者直接运行代码并让解释器来翻译。后者是Python(以及PHP、Ruby和类似的“脚本”语言)的工作原理。

  

  硬件怎样知道如何在内存中存储这些0和1 呢? 软件,我们的代码,需要告诉它如何为这些数据分配内存。什么样的数据?这取决于语言对数据类型的选择。

  每种语言都有数据类型。它们通常是你学习如何编程时最先学习的东西之一。

  你可能会看到这样一篇教程(出自Allen Downey的优秀著作——《像计算机科学家一样思考》),它讲述了数据类型是什么。简单地说,它们是表示内存中数据存放的不同方式。

  

  数据类型有字符串、整数等等,这取决于你使用的是哪种语言。例如,Python的基本数据类型包括:

  

  还有由其他数据类型组成的数据类型。例如,Python列表可以包含整数、字符串或两者。

  为了知道要分配多少内存,计算机需要知道存储的数据类型。幸运的是,Python有一个内置函数getsizeof,它会告诉我们每种数据类型的大小(以字节为单位)。

  这个奇妙的答案给了我们一些“空”数据结构的近似定义:

  

  如果我们对它进行排序,我们可以看到默认情况下最大的数据结构是一个空字典,然后是一个集合。整数和字符串相比就很小了。

  这让我们知道程序中不同的类型占用多少内存。

  我们为什么要在意?有些类型比其他类型更有效率,更适合不同的任务。其他时候,我们需要对这些类型进行严格的检查,以确保它们不会违反我们程序的某些假设。

  但这些类型究竟是什么,我们为什么需要它们?

  这就是类型系统发挥作用的地方。

  很久以前,在一个遥远的星系里,人们手工做数学运算时发现,如果他们用“类型”来标记数字或方程的元素,他们就可以减少用数学证明这些元素时所遇到的逻辑问题。

  因为在最初的计算机科学中,基本上是手工做大量的数学运算,一些原理被继承了下来,类型系统成为一种通过向特定类型分配不同的变量或元素来减少程序中bug数量的方法。

  几个例子:

  如果我们在为银行编写软件,那我们就不能将字符串放在计算某人账户总额的代码段中。如果我们正在处理调查数据并想知道某人是否做了某些事情,那选用布尔型Yes/No作为答案可能会将工作做到最好。在一个很大的搜索引擎中,我们必须限制允许人们放入搜索框中的字符数,因此,我们要对某些特定类型的字符串进行类型校验。

  今天,在编程中,有两种不同的类型系统:静态系统和动态系统。Steve Klabnik将其解释如下:

  静态类型系统是一种机制,编译器通过这种机制检查源代码并将标签(称为“类型”)分配给语法片段,然后使用它们来推断程序的行为。动态类型系统是一种机制,编译器通过这种机制生成代码来跟踪程序使用的数据种类(碰巧也称为“类型”)。

  这是什么意思呢?这意味着,对于编译型语言来说,你通常需要预先标记类型,以便编译器可以在程序编译时检查它们,从而确保程序有意义。

  这无疑是我最近读到的关于两者区别的最好解释:

  我过去使用过静态类型语言,但过去几年的编程工作主要是用Python编写的。一开始这种体验有点烦人,它让我觉得它只是让我慢下来,迫使我变得非常直截了当,而Python只是让我做我想做的,即使我偶尔会出错。这有点像给一个总是打断你,并让你解释清楚你的意思的人下指令与给一个总是点头附和,似乎能理解你的人下指令相比,尽管你并不确定他们总是能听懂你说的话。

  我花了一段时间才理解这里的一个小警告:静态类型语言和动态类型语言是紧密联系的,但和编译型或解释型语言不是同义的。你可以对一个动态类型语言(如Python)进行编译,也可以对静态语言(如Java)进行解释,例如,当你使用Java REPL时。

  那么这两种语言中的数据类型有什么不同呢?在静态类型中,必须预先设计好你的类型。例如,如果你在Java中工作,你会有这样一个程序:

  

  如果你注意到程序的开始部分,你会看到我们声明了一些变量,并带有一个这些类型是什么的指示符:

  

  我们的方法还必须包含我们放入其中的变量,这样我们的代码才能正确编译。在Java中,你必须从头开始规划你的类型,以便编译器在将代码编译成机器码时知道它要检查什么。

  Python对用户隐藏了这一点。类似的Python代码是:

  

  Python如何处理数据类型?

  Python是动态类型的,这意味当你运行程序时它只检查你指定的变量的类型。正如我们在示例代码中看到的,你不必预先计划类型和内存分配。

  处理的过程是:

  在Python中,它会使用CPython将源代码编译成一种更简单的形式,称为字节码。这些指令在本质上类似于CPU指令,但是它们不是由CPU执行的,而是由称为虚拟机的软件执行的。(这些虚拟机不是模拟整个操作系统的VM,而只是一个简化的CPU运行环境。)

  当CPython构建时,如果我们不指定变量的类型,它如何知道那些变量是什么类型?它不知道。它只知道变量是对象。Python中的所有东西都是一个对象,直到它不是(也就是说,当它变成了一个更具体的类型),我们才会具体地检查它。

  对于字符串之类的类型,Python会假设任何由单引号或双引号括起来的东西都是一个字符串。对于数字,Python会选择一个数字类型。如果我们试图对该类型做些什么,而Python不能执行该操作,它稍后会告诉我们。

  例如,如果我们试着:

  

  它会告诉我们它不能将一个字符串和一个浮点数相加。在知道name是一个字符串,seconds是一个浮点数之前,这段代码无法正确运行。

  换句话说, Duck类型的出现是因为当我们进行加法时,Python并不关心一个对象是什么类型。它所关心的是对它的加法方法的调用是否会返回任何合理的值。如果没有返回,系统会捕获一个错误。

  那么,这意味着什么呢?如果我们尝试用与Java或C相同的方法编写程序,那么在CPython解释器执行有问题的那一行之前,不会出现任何错误。

  事实证明,对于处理较大型代码库的团队来说,这是不方便的,因为你不是在处理单个变量,而是处理相互调用的类之上的类,并且需要能够快速检查所有教程内容。

  如果你不能为它们编写良好的测试,并在运行于生产环境之前让它们捕捉到错误,那么你可能会破坏系统。

  一般来说,使用类型提示有很多好处:

  如果你正在处理复杂的数据结构,或者具有大量输入的函数,那么在编写代码之后很久以后,你仍旧能够更容易看到这些输入是什么。如果你有一个只带一个参数的函数,就像我们这里的例子,它是相当简单的。

  但是,如果你正在处理一个包含大量输入的代码库,比如这个来自PyTorch文档的例子,又会怎样呢?

  

  模型是什么?啊,我们可以深入到代码库中,可以看到它是

  

  但是如果我们可以在方法签名中定义它,那我们就不需要进行代码搜索,不是很酷吗?也许就像

  

  device是怎样的呢?

  

  torch.device是什么?这是一种特殊的PyTorch类型。如果我们查看文档和代码的其他部分,我们会发现:

  

  如果我们能注意到这一点,那我们就不用查找了,不是很好吗?

  

  等等。因此,类型提示对写代码的人,也就是你,是很有帮助的。

  类型提示对于其他人阅读你的代码也很有帮助。阅读别人已经标记好的代码要容易得多,而不需要像上面那样进行搜索。键入提示可以增加可读性。

  那么,Python做了什么来实现与静态类型语言相同的可读性呢?

  这就是类型提示的作用。作为补充说明,文档可以互换地将它们称为类型注释或类型提示。我将称其为类型提示。在其他语言中,注释和提示的含义完全不同。

  在Python2中,人们开始在他们的代码中添加提示来给出各种函数返回值的信息。

  这样的代码最初看起来是这样的:

  

  类型提示以前只是评论。但是,Python开始逐渐转向一种更统一的处理类型提示的方法,这些方法开始包括函数注释:

  

  随着PEP484的发展,它与mypy被一起开发出来,mypy是出自DropBox的一个项目,它会在你启动程序时检查类型。请记住,在运行期间是不检查类型的。只有当你尝试在一个不兼容的类型上运行一个方法时,才会出现问题。例如,尝试对字典进行切片或尝试从一个字符串中弹出值。

  从实现细节来看,虽然这些注释在启动时通过普通的annotations属性可以使用,但在运行时不会进行类型检查。相反,该建议假定存在一个独立的离线类型检查器,用户可以自动在其源代码上运行。本质上,这样一个类型检查器充当一个非常强大的linter。(当然,单个用户也可以在运行期间将类似的检查器用于契约式设计和JIT优化,但这些工具还不够成熟。)

  那么这在实际情况中是怎样的呢?

  类型提示还意味着你可以更容易地使用IDE。例如,和VS Code一样,PyChamr也提供了基于类型的代码完成和检查。

  类型还有一个好处: 它能防止你犯愚蠢的错误。这是一个它怎样阻止犯错的很好的例子:

  假设我们正在向一个字典添加名字

  

  如果我们允许这种情况发生,我们的字典中就会有一堆格式不正确的条目。

  我们如何解决这个问题?

  

  通过对其运行mypy来解决:

  

  我们可以看到mypy不允许这种类型。将mypy包含在一个管道中并在你的持续集成管道中进行测试是有意义的。

  使用类型提示的最大好处之一是,你可以在IDE中获得与使用静态类型语言相同的自动完成功能。

  例如,假设你有这样一段代码。这只是我们之前的两个函数,被封装在类中。

  

  一个简单的事情是,现在我们已经(自由地)添加了类型,我们可以实际看一下当我们调用类方法时会发生什么:

  

  

  mypy文档中对于开始输入代码库有一些很好的建议:

  

  要开始为你自己的代码编写类型提示,你需要了解以下几点:

  首先,如果你使用的是字符串、整数、布尔型和基本的Python类型之外的任何类型,那么你就需要导入类型模块。

  其次,通过该模块你可以获得几种复杂的类型:Dict、Tuple、List、Set等等。

  例如,Dict[str, float]表示你要检查一个字典,其中键是字符串,值是一个浮点数。

  还有一种类型称为Optional和Union。

  第三,这是类型提示的格式:

  

  如果你想进一步了解类型提示,许多聪明人已经编写了教程。

  那么,结论是什么?用还是不用?

  但是你应该开始使用类型提示吗?

  这取决于你的用例。正如Guido和mypy文档所说的,

  mypy的目的并不是要说服所有人都编写静态类型的Python—不管是现在还是将来,静态类型都是完全可选的。其目标是为Python程序员提供更多的选项,使Python在大型项目中成为比其它静态类型语言更具竞争力的选择,从而提高程序员的工作效率和软件质量。

  由于设置mypy和考虑所需类型的开销,类型提示对于较小的代码库和实验(例如,在Jupyter笔记本中)来说没有意义。什么是小型代码库?保守地说,可能是低于一千行的代码。

  对于较大的代码库、与他人协作的地方和包、具有版本控制和持续集成系统的地方来说,它是有意义的,可以节省大量时间。

  我的观点是,在接下来的几年里,类型提示将会变得更加普遍,即使不是很普遍的话,提前开始使用也没有坏处。

  致谢

  特别感谢 Peter Baumgartner, Vincent Warmerdam, Tim Hopper, Jowanza Joseph, 和 Dan Boykis 对本文草稿的阅读。所有剩下的错误都是我的 :)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值