Python强类型编程最佳实践——PyCon2020分享

本文涉及的产品
对象存储 OSS,20GB 3个月
日志服务 SLS,月写入数据量 50GB 1个月
文件存储 NAS,50GB 3个月
简介: 本文介绍我在PyCon2020年底时分享的议题内容,结尾有录播的视频和PPT下载链接。Python对于强类型检查还是符合其核心精神(灵活性与实用性),已经非常完善,且大踏步的往前延伸,另一方面,也又一次的让Python的深入掌握的门槛进一步增加(进入了强类型编程、泛型编程领域,甚至动态扩展的场景)。本文介绍Python强类型的历史背景以及22个最佳实践和使用工具与策略,帮助快速掌握Python强类型检查的核心与策略。

引言

本文介绍我在PyCon2020年底时分享的议题内容,结尾有录播的视频PPT下载链接


Python强类型检查的历史、背景


Pyton语言与类型限定


我们都知道Python是一门强类型的动态类型语言,具体如下特性:

  • 可以动态构造脚本执行、修改函数、对象类型结构、变量类型
  • 但不允许类型不匹配的操作


这里举了2个例子,第一个例子体现动态性:用字符串直接执行代码,动态构建了一个函数并执行,甚至给函数挂载新的名字。第二个例子体现强类型性:变量都有类型信息,不同类型无适配操作时不允许操作,例如整数和字符串不允许相加。

image.png                                  


动态语言 vs 类型提示

因为Python的强类型挂载,但是又不像C++/Java有强类型定义信息(C++11开始也有auto自动推导了),比较容易出错。因此Python也提供了类型标注功能,有了类型标注提示后,就可以在编码时即发现错误。


例如下面一个有标注,一个没有标注的例子;可以看到有标注的例子,IDE/工具都会在编码时即发现潜在与标注不匹配的执行甚至逻辑错误并提示,而无标注时,工具的支持就大打折扣了。

image.png


函数标注(PEP 3107

标注功能是通过PEP 3107就添加进来了,以函数的标注为例,实际上对函数参数、返回值标注,是放在add.__annotations__变量里面。如图,IDE、工具可以在代码层面静态检查并提示错误,信息非常直观。

image.png

强类型检查的优势与现状

通过上面的例子就可以发现,强类型检查可以给Python带来不少好处,这方面总结的文章不少,这里总结一下如下:

  • 易读:比docstring更易理解接口协议,也更易使用三方库(IDE工具支持)
  • 排错:编码、编译期间即可发现错误(IDE工具支持)。
  • 重构:接口范围明确,更易于理解和放心重构。
  • 性能:(可能)可以做到静态编译优化。


那么实际落地的情况如何呢?可以看到还是乐观的:

  • 生态-IDE三方类型检查工具比较全面(PyCharmVSCodeVIM等)。
  • 生态-库工具:三方库有官方或大厂支持且很流行。
  • 官方/mypy-9.5K starFacebook/pyre-5K StarGoogle/pytype-3K star等。
  • 成熟度:静态检查已被广泛验证有效性,尤其大型工程(dropbox迁移400万行到静态标注)。
  • 行业:主流编程语言以静态强类型检查为主(C++JavaGo等);其他动态语言的静态类型扩展迅猛发展(如TypeScript)。


Python强类型检查的策略

既然强类型有这么多好处,并且已经越来越被接受,是不是意味着Python会往强类型语言转型呢?

可以确认不会发生(至少在Python3上),因为Guido很早就在正式文档中阐述了这方面的策略:


  • Python will remain a dynamically typed language, and the authors have no desire to ever make type hintsmandatory, even by convention.  
                                                      ——Guido in PEP 484


可以看到Python的策略是保留了类型检查向后兼容的能力,不会转型为真正的强类型检查语言,但是Python依然会对强类型检查做持续的支持,通过如下金字塔结构来完成,最终达到即享受强类型检查的好处,又保持向后兼容和动态类型语言的灵活性,根据后续的介绍,可以看到,这方面Python还是付出了不少的努力。


image.png


Python类型系统到Py3.72018.6)才基本成型

具体来看下Python做了哪些事情来支持强类型检查,如下是一个非常长的(14个)PEP(Python改进建议)的列表和落地情况(图中时间是文档时间+1年左右是实际落地时间),可以看到2006年Python就引入了一些基础支持,并且在PEP 483开始快速迭代(图中橙色是比较重要的迭代),并且到了Python3.7才真正算是成型。


image.png


Python强类型检查常用场景实践(22个)

下面开始浏览一下Python强类型检查的能力在各个主要场景下的具体最佳实践


典型类型标注


场景1:复杂数据结构的标注(使用别名)(PEP 484)

当被标注的类型比较复杂时,可以使用类型别名简化类型提示标注。(这里直接使用了内置类型[]做标注(PEP 585))

这个例子中servers是一个比较复杂的结构,它的每个成员可以用一个变量(别名)接住后再继续定义,最后用于标注。

image.png


场景2:变参类型标注(PEP 484)

Python的变参也支持标注,如下案例解释了如何对变参、命名变参直接标注其值的类型:

image.png


场景3:类别选择 Union vs. TypeVar(PEP 484)

Python的动态灵活性,允许一个变量的类型可以是多个选择(例如一个add函数允许传入int、long、str类型的变量等),在这种情况下可以Union和TypeVar来定义。两者区别如下:

  • 使用Union定义可能的类型选择: xy、返回值的类型同一时刻可以不同
  • 使用TypeVar定义一组类型:xy、返回值的类型同一时刻必须相同


以下举例说明了两者区别:

image.png


场景4:函数类型(PEP 484)

高阶函数可以接收或返回函数,因此函数和变量一样,也需要类型标注,具体形式可以如下:

  • Callable[[参数1类型, 参数2类型, …], 返回类型]


例如下面这个高阶函数的参数标注:

image.png


场景5:泛型类型(PEP 484)

容器下的泛型:TypeVar

泛型在C++/Java中非常普通,Python中也存在这样的情况,例如参数需要是一个列表,但是里面元素类型可以不定。这种情况,可以使用泛型TypeVar例如下面这个函数接受一个泛型列表,并返回第一个元素。在标注后,错误的使用情况也可以被检查出来。

image.png


当泛型是多个可选类型时,TypeVar也支持,如下例子:

image.png


类下的泛型:Generic

泛型场景比较丰富,类也有泛化需求,例如一个类需要定义泛型时,比较好的方式是,使用Generic并继承它,参考如下案例:

image.png


以上就各种情况的标注做了大概的介绍,可以看到Python对各种类型的标注都可以很好支持,下面具体讲解一下标注的方式。



标注方式

场景6:使用注释(Comments)进行标注(Annotation)(PEP 484)

可以看到,目前为止的标注都是以修改代码才能完成的,这样某种程度上是有一定风险的,因为修改了代码,就意味着可能引入了BUG,哪怕风险不高。这个问题实际PEP已经考虑到了,这并不是Python添加强类型检查的唯一方式。


除了上面的代码方式标注,还可以用注释进行标注。


形式如下:

  • # type: xxxx 进行标注
  • 注意其中函数的标注方式:
  • (参数类型列表) -> 返回值类型


这里举一个例子说明:

image.png


场景7:将标注放在单独文件(PEP 484)

添加注释某种程度上不算修改源码,但还是会修改源代码文件(依然引入了一些风险,并且可能源代码不一定有权限去添加注释),因此PEP 484还引入独立的文件方式来解决这个问题:


具体的方法是,使用一个独立的Stub文件(.pyi)与源文件相同,并放在同一个文件夹即可。而Pyi的语法是将源文件的一些类、函数、变量等都打上标注(不需要具体实现)。


例如:

image.png


这样的方法有优点也有缺点:

  • 优点:
  • 不需要修改源代码(减少引入BUG可能)
  • Pyi可以使用最新的语法(源文件可以是低版本)
  • 测试友好
  • 不拥有的三方库,也可以补充标注信息。
  • 缺点:
  • 代码重复写了一遍头(工作量)
  • 打包变得复杂



高级类型标注

场景8:前置引用(PEP 484)


强类型检查实际是是非常复杂的(参考C++模板技术与泛型编程),在进行类型标注时,实际的扩展的情况要复杂的多,下面开始讲解一些高阶的强类型标注的场景。


首先看一个经典案例是一个二叉树节点,在进行类型标注是,就需要引用自己,改如何解决?


这种情况下就需要使用前置引用,方式是用字符串代替类型标注。参考下面例子:

image.png


场景9:函数标注扩展overload(PEP 484)

我们都知道Python函数语法本身不支持重载(实际通过库可以扩展),一般一个函数可以接受各种情况的参数,并返回特定的类型。例如一个add,传递int就返回int,传递bytes/str就返回str,这种情况下应该如何标注呢?


可以使用typing.overload进行参数返回值描述。参考下面的案例,但需要注意:

  • 必须有一个无修饰版本做真正实现
  • 这里仅仅用于静态类型检查,如果需要真正的重载,可以使用functools.singleddispatch

image.png


有时可以用TypeVar代替,显得更简洁,但请注意和上述overload有一个重要区别,就是入参与返回参数的映射关系丢失了。

image.png


场景10:协变(covariant)与逆变(contravariant) PEP 484

在泛型编程中,还有一个比较常见的概念就是协变与逆变(Java泛型编程中常常遇到),如下定义:

  • 协变:
  • 让一个粗粒度接口(或委托)可以接收一个更加具体的接口(或委托)作为参数(或返回值);
  • 例如:老鹰列表赋值给列表
  • 逆变:
  • 让一个接口(或委托)的参数类型(或返回值)类型更加具体化,也就是参数类型更强,更明确。
  • 例如:列表赋值给老鹰列表


这种情况在Python强类型标注也一样存在,例如下面是一个协变的例子,注意使用了参数covariant=True

image.png



场景11:静态duck typing(PEP 544)

Python支持OOP,也支持duck typing,在强类型检查中,duck typing也一样适用,例如一个函数close_resource接受一个参数,要求这个参数必须提供一个特定的方法.close()。这种需求在强类型检查时,也可以支持,我们称之为静态duck typing,相关方案由PEP 544支持。


如下是一个具体案例:

image.png

通过继承typing.Protocol,构建了一个IResource的静态类型,再使用这个IResource进行标注,之后就可以实现对所有参数是否具备这个IResource定义的方法进行校验。这就是static duck typing。


某种程度上,可以静态duck typing类似于Java的接口概念。

  • 可以有实现,可以被继承
  • 可以继承多个接口,构建一个新接口(参考后面的例子)


但实际类型检查时,不要求被标注对象继承这个IResource,只需要对象的所有成员方法signature匹配Protocol即可。


如下是一个多继承的例子:

image.png


场景12:泛型静态duck typing(PEP 544)

通用的,泛化在静态duck typing也存在,例如约束的方法中的某个参数的类型,需要是泛化的。这种情况,typing.Protocol也是支持。


和前面一样,只需要使用TypeVar定义个泛型类型,再传递给Protocol即可。参考下面样例,这里定义了一个标准可迭代对象的泛型静态duck typing。

image.png

同样的,也支持泛型编程中的协变或逆变:

image.png


场景13:运行时化duck typing(PEP 544)

静态duck typing,顾名思义,静态时(编码时)检查,运行时不检查。但有时,也存在运行时使用isinstance检查的需求,这种情况下,这种需求也是支持。只需要使用runtime_checkable装饰,即可让Protocol可以运行时访问,例如isinstance检查。


参考如下例子:

image.png



有用的标注库

场景14:dataclasses(PEP 557)

Python对强类型编程的支持不限于标注类型的支持,也提供了许多基于强类型标注上结合Python自身灵活性扩展的非常强大库来增加工程效率,这里我们介绍一个相关的非常有用的库dataclasses。


  • 这个库顾名思义,支持数据类的快速构建,可以把dataclass理解为一个加强版的namedtuple,是一个支持带默认值的可变的命名元祖:

image.png

  • 甚至还支持延迟初始化,如下的变量c是基于a和b的值,动态初始化的。

image.png

  • 也支持复杂的默认值,例如下面的mylist不传入时,会使用空list初始化。

image.png

  • dataclasss也提供了与tupledict互转方法,如下:

image.png



场景15:字面量类型(PEP 586)

Python较新的发布,继续发挥其灵活性的特点,不限于借鉴其他语言的强类型机制,还发扬光大,添加许多更灵活有用的功能,例如这个字面量类型。


有了这个功能后,可以做到不修改类型为enum的情况下,限定传递参数。

语法形式:Literal[字面量1, 字面量2, …]

例如下面例子,使用Literal,限定了打开文件的mode字符串的可枚举值,编码期间就可以检查非法的参数:

image.png


场景16:typing.Final(PEP 591)

通用的,在Python 3.8版本中引入一个扩展的标注typeing.Final,用于标注变量,这个内置标注非常有用,可以理解为实现了C++语法const的静态检查作用。

  • 指定变量被初始化后无法再被修改、类变量无法被子类修改。
  • 声明为Final的类成员的变量,未初始化的,必须在__init__里面初始化。


如下案例具体说明:

image.png


场景17:typing.final(PEP 591)

另一个被引入的就是小写的typing.final,用于标注类,可以理解为实现了Java语法final的静态检查作用。虽然Python本身就可以扩展实现运行时final的作用,但是这里实现了检查期的final,这个官方版本可以说非常的有用。


如下,除了可以修饰类(不能被继承),甚至可以修饰类的方法(不能被重写)。

image.png


优化、控制与扩展


场景18:延迟类型提示执行(PEP 563)

我们知道,Python标注执行期间不做强类型检查,但实际上标注依然会占用资源(空间和时间),下面开始我们看一些性能优化的场景。


例如下面的是一个极端例子,标注初始化指向了一些代码逻辑,初始化时,需要额外的资源消耗。


为了优化,可以开启延迟类型提示执行,只需要从__future__导入annotations即可,这时会跳过标注的代码逻辑,直接转化为字符串。


参考下面案例:

image.png


场景19:静态检查时与运行时区分(PEP 484)

可以看到类型检查需要导入一些库,也就增加了运行时的开销,因此Python引入检查时的概念,允许在检查时才导入特定库,而在实际运行时不导入。但是注意这种情况下,相关标注只能用注释或字符串(前置)方式标注。


例如下面的例子,可以即保持静态检查,又不会因为引入特定标注库对性能和资源产生冲击:

image.png


场景20:关闭静态类型检查(PEP 484)

特定情况下,我们也需要关闭静态检查(例如测试或开发中间时),这种情况只需要使用typing.no_type_check修饰函数或类来关闭。但是如果希望关闭对一个装饰器的静态检查的话,需要使用typing.no_type_check_decorator修饰装饰器来关闭。

image.png


场景21:新的Hook方法(PEP 560)

因为typing中常常需要使用[]进行传递类型,在定义自己的类型时,可能需要做一些高级定制,因此Python-Core中引入了新的Hook方法__class_getitem__,在传入类型时会自动调用。这样的修改,简化typing的实现方式。

image.png


场景22:类型标注(PEP 593)

Python再一次发挥了灵活性特点,并大步向前。这个是Python3.9才引入的新特性,已经延伸了强类型检查的范围,扩展到的运行时数据逻辑性的问题,将以前一些三方数据标注库从运行时引入到了检查期。


通过使用typing.Annotated,可以进一步增加额外附属信息,方便静态检查或运行时做更进一步的检查。

  • 例如:分数(值从1-100的整数类型)

image.png

  • 又例如:包含最多10个元素的元组列表

image.png


结语

以上可以看到Python对于强类型检查还是符合其核心精神(灵活性与实用性),一方面还是非常完善的,且大踏步的往前延伸,另一方面,也又一次的让Python的深入掌握的门槛进一步增加(进入了强类型编程、泛型编程领域,甚至动态扩展的场景)。


工具参考

如下列前面提到的一些三方工具,供进一步参考。

  • typeshed: Python内置标准和三方库的pyi集合repoPyCharmmypypytype已包含)
  • mypy: 官方标准静态类型检查工具。
  • pyreFacebook开源的静态类型检查工具。
  • pytypegoogle开源的Python静态代码扫描工具(不依赖标注)。
  • pyannotatedropbox开源的自动给Python添加类型标注的工具。
  • pydantic:一个基于标注的Python数据校验与配置管理库。


什么时候需要或不需要类型检查?

了解背景并讲解具体场景前,我先看一下具体的使用策略:什么时候需要或不需要使用Python类型检查?


以下情况建议采用:

  • 提供SDK、库/接口给其他人时。
  • docstring更清晰、主流IDE支持提示和校验。
  • 代码行数越多,价值越大。
  • 规范化编码,通过工具可以辅助发现潜在BUG
  • 需要写UT单元测试)的地方,就需要类型检查 (by Bernat Gabor)


以下情况需要一定考量再决定:

  • 原型(Prototype)或验证性质项目(POC)的代码,可以先不引入。
  • 大量旧有代码,需要逐步阶段性引入(参考Dropbox经验)。
  • 不熟悉Python和类型提供功能用法时,可以先不引入。


其他

录播视频:https://www.bilibili.com/video/av458282564/

PPT下载: https://github.com/wjo1212/ChinaPyCon2020


顺便打个广告,欢迎扫群加入阿里云-日志服务(SLS)技术交流钉钉群, 获得第一手资料与支持:

网络异常,图片无法展示
|

目录
相关文章
|
17天前
|
存储 数据挖掘 开发者
Python编程入门:从零到英雄
在这篇文章中,我们将一起踏上Python编程的奇幻之旅。无论你是编程新手,还是希望拓展技能的开发者,本教程都将为你提供一条清晰的道路,引导你从基础语法走向实际应用。通过精心设计的代码示例和练习,你将学会如何用Python解决实际问题,并准备好迎接更复杂的编程挑战。让我们一起探索这个强大的语言,开启你的编程生涯吧!
|
23天前
|
机器学习/深度学习 人工智能 TensorFlow
人工智能浪潮下的自我修养:从Python编程入门到深度学习实践
【10月更文挑战第39天】本文旨在为初学者提供一条清晰的道路,从Python基础语法的掌握到深度学习领域的探索。我们将通过简明扼要的语言和实际代码示例,引导读者逐步构建起对人工智能技术的理解和应用能力。文章不仅涵盖Python编程的基础,还将深入探讨深度学习的核心概念、工具和实战技巧,帮助读者在AI的浪潮中找到自己的位置。
|
23天前
|
机器学习/深度学习 数据挖掘 Python
Python编程入门——从零开始构建你的第一个程序
【10月更文挑战第39天】本文将带你走进Python的世界,通过简单易懂的语言和实际的代码示例,让你快速掌握Python的基础语法。无论你是编程新手还是想学习新语言的老手,这篇文章都能为你提供有价值的信息。我们将从变量、数据类型、控制结构等基本概念入手,逐步过渡到函数、模块等高级特性,最后通过一个综合示例来巩固所学知识。让我们一起开启Python编程之旅吧!
|
23天前
|
存储 Python
Python编程入门:打造你的第一个程序
【10月更文挑战第39天】在数字时代的浪潮中,掌握编程技能如同掌握了一门新时代的语言。本文将引导你步入Python编程的奇妙世界,从零基础出发,一步步构建你的第一个程序。我们将探索编程的基本概念,通过简单示例理解变量、数据类型和控制结构,最终实现一个简单的猜数字游戏。这不仅是一段代码的旅程,更是逻辑思维和问题解决能力的锻炼之旅。准备好了吗?让我们开始吧!
|
10天前
|
Python
Python编程入门:从零开始的代码旅程
本文是一篇针对Python编程初学者的入门指南,将介绍Python的基本语法、数据类型、控制结构以及函数等概念。文章旨在帮助读者快速掌握Python编程的基础知识,并能够编写简单的Python程序。通过本文的学习,读者将能够理解Python代码的基本结构和逻辑,为进一步深入学习打下坚实的基础。
|
14天前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
17天前
|
存储 人工智能 数据挖掘
Python编程入门:打造你的第一个程序
本文旨在为初学者提供Python编程的初步指导,通过介绍Python语言的基础概念、开发环境的搭建以及一个简单的代码示例,帮助读者快速入门。文章将引导你理解编程思维,学会如何编写、运行和调试Python代码,从而开启编程之旅。
36 2
|
18天前
|
存储 Python
Python编程入门:理解基础语法与编写简单程序
本文旨在为初学者提供一个关于如何开始使用Python编程语言的指南。我们将从安装Python环境开始,逐步介绍变量、数据类型、控制结构、函数和模块等基本概念。通过实例演示和练习,读者将学会如何编写简单的Python程序,并了解如何解决常见的编程问题。文章最后将提供一些资源,以供进一步学习和实践。
30 1
|
20天前
|
存储 网络协议 IDE
从零起步学习Python编程
从零起步学习Python编程
|
19天前
|
机器学习/深度学习 存储 数据挖掘
Python 编程入门:理解变量、数据类型和基本运算
【10月更文挑战第43天】在编程的海洋中,Python是一艘易于驾驭的小船。本文将带你启航,探索Python编程的基础:变量的声明与使用、丰富的数据类型以及如何通过基本运算符来操作它们。我们将从浅显易懂的例子出发,逐步深入到代码示例,确保即使是零基础的读者也能跟上步伐。准备好了吗?让我们开始吧!
25 0