学习
实践
活动
专区
工具
TVP
写文章

Python中的类型提示(上)

关键时刻,第一时间送达!

Python最大的卖点之一就在于它动态的数据类型。没有计划想改变python的这一特点。不过在2014年的9月,Guido van Rossum (python开源社区的领导者)创建了一项python改进提议 (PEP-484) ,将类型提示加入到python语言中。一年后,也就是2015年的9月,这项功能作为Python3.5.0版本中的一种特性得以面世。也就是说,在Python诞生了25年以后,终于有一种标准的方法可以在Python代码中添加类型信息了。在这篇博客中,我将前去探索类型提示这个系统是如何走向成熟的,我们应该如何去使用它,以及类型提示功能的下一步发展会是怎样的。

声明:在这篇博客中,你将会看到许多海豹和企鹅的图片,这主要是因为我个人对这些动物的喜欢,再说了,没有什么比可爱的动物们更能帮助我们理解消化这些复杂的问题了,不是吗?

为什么我们需要类型提示?

类型提示是用来做什么的?

首先,让我们来了解下为什么在python中需要类型提示。这项功能有很多优点,我会试着将这些优点按重要性排序一一列举出来。

1、使代码更容易看懂

清楚每一个参数的数据类型会让理解和维护代码库容易很多。比如,假设你有一个函数。在刚设计好这个函数的时候,我们都知道函数中参数的类型,但几个月过后情况就完全不同了。在代码旁边表明所有参数和返回值的类型将显著的提高理解代码片段的速度。要知道,你用来读一段自己以前写的代码上的时间要远长于用来写它的时间。因此,你应该优化代码的可读性。

类型提示功能可以在你调用一个函数时提示你需要传入什么类型的参数,或者在你需要扩展/修改函数时告诉你函数输入输出两端的数据类型。举个例子,想象下图中用于发送请求的函数

只要看看标记我就可以知道request_data可以是任何类型,headers的内容是一个字符串字典。用户信息是可选项(默认值为None)或它还需要UserId的值。同时as_json的约定是它必须是一个布尔值,其本质是是一个标志,但单从名称上可能不能立刻看出这一点。

事实上,很多人都已经明白了类型信息是必要的,但是直到现在,因为缺少更好的选择,类型信息经常被记录在文档中。类型提示系统将这些信息移动到了更接近函数接口的位置,并提供一种优秀的定义方法来满足声明复杂数据类型的需求。你可以建造一些linter工具,并且在每次修改过代码后都运行这些工具来检查这些类型提示约束以来确保它们永远不会过时。

2、更容易重构

当你在尝试重构你的代码库时,类型提示功能使得寻找一个特定的类在何处使用过变的非常容易。虽然很多IDE已经有了类似的功能来实现这一点,类型提示可以让它们的检测范围和准确率都变成100%。通常情况下,会对数据类型是如何在你的代码中演变的提供更平滑更准确的检测。要牢记动态类型意味着任何变量的数量类型都有可能发生改变,且所有的变量在某一时刻有且只有一种类型。数据类型这个系统依旧属于编程的核心组成部分。时刻谨记,你应该用isinstance来驾驭应用的逻辑。

3、更容易使用库

使用类型提示功能意味着IDE拥有了一个更加准确更加聪明的建议引擎。现在当你调用自动补全功能时,IDE完全清楚哪些方法和属性在一个对象上是可用的。此外,如果用户试图调用某些不存在的东西或者传入类型错误的参数,IDE都可以立即发出警告。

4、类型linter工具

IDE对数据类型不正确的参数的建议很不错,对此做进一步提升的话可以使用linter工具来确保你应用中的数据类型是合乎逻辑的。运行这些工具可以帮你更早发现bug。(就像下面这个例子中,输入应该是字符串类型的,传入None类型会引发异常)

当然这只是一个简单的小例子,一些读者可能会提出这种不匹配的错误很容易发现。但是要知道linter工具在更加复杂的情况中,也即这类不匹配的错误越来越难被发现的情况中一样有效。比如嵌套函数调用:

尽管现在有越来越多的linter工具,python类型检测功能的实现参考的是mypy。mypy是一款python命令行应用,这也使得它很容易被集成进连续地一体化的流水线中去。

5、运行时的数据校验

类型提示可以被用于在运行时进行校验以确保调用方不破坏方法的规则。在也不用和一个包含数据类型提示信息的长列表一起启动你的函数了。取而代之,启用一个可以重复使用类型提示功能并在你的业务逻辑运行之前自动检查他们是否匹配的框架。(一个pydantic的例子):

类型提示不是设计用于做什么的?

从一开始,Guido就明确声明了类型提示功能并不是被设计出来以便在下面这些例子中使用的。(当然这并不意味着人们没有库或工具可用,开源力量的胜利!)

1、在运行时不能进行数据类型推断

运行解释器(Cpython)在运行时不会试图去推断类型信息,也不会对传递的参数进行任何的类型验证。

2、不能提高性能

运行解释器(Cpython)不会使用类型信息来优化其产生的字节码的安全性或执行效率。在执行一个python脚本时,类型提示会被解释器当作注释丢弃掉。

上述内容的重点在于类型提示功能是为了提高开发者的体验而生的,不影响你的程序的计算方式。它带来的是快乐的程序员而不是效率更高的代码!

数据类型系统是什么样的?

Python中的类型提示是渐变的,也就是说对于给定的函数或者变量在没有指定类型提示时,我们就假设它可以拥有任何类型(依然是一个动态的数据类型)。一次为一个函数或一个变量添加类型提示,逐渐地让你的代码具有数据类型的意识。可能需要添加类型提示的对象有:

· 函数参量

· 函数返回值

· 变量

记住只有添加过类型提示的代码才可以进行类型检查。在这些代码上使用linter工具(比如mypy)时,如果有类型不匹配的错误你会得到错误提示:

这段代码会得到如下的结果:

注意,我们可以检测传入的参数的类型的不匹配问题以及访问对象上不存在的属性的问题。甚至在后来提出了有效性检查作为可选项,让检查和修改拼写错误变得更容易。

如何添加类型提示

一旦你决定添加类型提示,你会意识到有不止一种方法可以将其添加到代码库中。一起来看下你都有哪些选择。

1、类型标注

类型标注是一种直接的方法,同时也是在typing文档中最常被提到的方法。类型标注使用函数标注(详见PEP-3107,Python3.0+)和变量标注(详见PEP-526,Python3.6+)。这些允许你使用:给变量和函数参数附加信息,使用->给函数或方法的返回值附加信息。

这种方法的优点是:

√ 这是一种规范的做法,这意味着这是所有方法中最简洁的。

√ 因为这些类型信息是直接附加在代码旁边的,这意味着你已经将这些数据打包了出来。

这种方法的缺点是:

· 它不能向后兼容,你需要Python3.6及以上版本才能使用这项功能

· 它会强迫你导入所有数据类型的依赖,尽管这些信息在运行时完全不会用到。

· 在类型提示中,你可以使用组合类型,比如List[int]。为了创建这些复杂类型,在第一次加载这些文件是,解释器确实需要进行一些额外的操作。

最后两点和我们前面介绍的类型系统的初衷相违背,也就是在运行时将类型提示作为注释来处理。为了在一定程度上解决这个问题,Python3.7版本引入了PEP563-标注延迟处理。一旦你加入这行代码:

解释器将不会构建这些组合结构。一旦解析完脚本的语法树,解释器会识别类型标注并跳过对它的运算,并不加修改的保存下来。这个机制实现了仅在需要的时候对类型标注进行解释:比如在运行类型检查时由linter工具来处理。在神话般的Python4问世后,这种机制应该会成为默认的行为。

2、类型注释

当标注语法不可用时,可以使用类型注释:

这样做,我们可以获得以下好处:

√ 类型注释可以在任何Python版本下工作。虽然类型库在Python3.5以上的版本中已经加入到了标准库,在Python2.7以上的版本中依然可以通过PyPi包来使用。此外,因为Python注释在任何版本的Python代码中都是有效的语言特性。这使得你可以在任何Python2.7及以上版本的代码中使用类型提示。使用的要求是:类型提示注释必须在函数或变量定义处的同一行或者下一行中;必须以type:开头。

√ 这个方案同样解决了打包问题,因为在打包后,代码中的注释很少会被删除。将类型提示信息和源程序一起打包可以让别人在使用你的库时获得更好的开发体验。

但是,这也产生了以下新问题:

· 一个缺点是,尽管类型信息的位置和参数很近,但并不是正好在参数旁边。这使得代码变得有些混乱。类型注释还必须在一行内完成。如果你有一个很长的类型提示信息,而且你的代码库有行的长度限制,那么就会导致问题。

· 另一个问题是,使用类型注释添加的类型提示信息会和其他使用注释标记的工具产生冲突。(例如,抑制其他linter工具产生的告警)。

· 除了强迫你导入所有类型信息之外,还会使你处于一个更危险的境地。现在,这些引入的类型都仅在代码中使用,这会让大部分linter工具认为这些导入都是没用的。如果你允许linter工具删除掉这些数据就会使得你的类型linter不能工作。值得注意的是,pylint将它的AST解析器升级成了类型AST解析器,从而修复了这个问题。并且将会在Python3.7发布后推出。

为了避免使用一行很长的代码作为类型提示,可以通过类型注释的方式一个一个的给参数添加类型提示,然后再放入行中。在返回值处使用类型标注:

让我们来快速使用一下,并看看类型注释是如何让你的代码变得更加混乱的。下面是一个相当简单的代码片段,其作用是在一个类中交换两个属性的值:

首先必须添加类型提示。因为类型提示会很长,你可以一个参数一个参数地添加:

(译者注:# type: (...)->Generator[Tuple[HasGetSetMutable,Optional[HasGetSetMutable]], None, None]不能分行,此处及下文中是为了图片规格的统一不得已而为之)

然而,等你需要引入你的类型时:

现在,这种样式的代码会在的静态的linter工具中产生错误的告警(例如在这里使用pylint),所以你需要为此添加一些用于抑制告警的注释:

完工了,尽管你把原本6行代码变成了16行。没错,更多需要维护的代码。只有在你的薪水是按所写代码的行数来支付的以及你的经理抱怨你表现的不够好时,扩大你的代码库才听上去像个好主意。

译者:舞象加冠

https://www.bernat.tech/the-state-of-type-hints-in-python/

Python开发整理发布,转载请联系作者获得授权

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180711B1M0TE00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

关注

腾讯云开发者公众号
10元无门槛代金券
洞察腾讯核心技术
剖析业界实践案例
腾讯云开发者公众号二维码

扫码关注腾讯云开发者

领取腾讯云代金券