在本指南中,你将了解Python类型检查。传统上,Python解释器以灵活但隐式的方式处理类型。Python的最新版本允许你指定可由不同工具使用的显式类型提示,以帮助您更有效地开发代码。
通过本教程,你将学到以下内容:
类型注解和提示(Type annotations and type hints)
代码里添加静态类型
静态类型检查
运行时强制类型一致
这是一个全面的指南,将涵盖很多领域。如果您只是想快速了解一下类型提示在Python中是如何工作的,并查看类型检查是否包括在您的代码中,那么您不需要阅读全部内容。Hello Types和正反两部分将让您大致了解类型检查是如何工作的,并介绍它在什么时候有用。
Type Systems
所有的编程语言都包括某种类型的系统,该系统将它可以处理的对象类别以及如何处理这些类别形式化。例如,类型系统可以定义一个数字类型,其中42是数字类型对象的一个例子。
动态类型
Python是一种动态类型语言。这意味着Python解释器仅在代码运行时进行类型检查,并且允许变量的类型在其生命周期内进行更改。以下示例演示了Python具有动态类型:
False:
1 +"two"# This line never runs, so no TypeError is raised
:
1 +2
...
3
"two"# Now this is type checked, and a TypeError is raised +
TypeError: unsupported operand type(s)for +:\'int\'and\'str\'
在上面例子中,if从未运行过,因此它未被类型检查过。else部分,当计算1 +“2”时,因为类型不一致所以,会产生一个类型错误。
如果改变一个变量的值的类型
"Hello" > thing =
> type(thing)
<class \'str\'>
28.1 > thing =
> type(thing)
<class \'float\'>
type()返回对象的类型。这些示例确认允许更改事物的类型,并且Python在更改时正确地推断出类型。
静态类型
与动态类型相反的是静态类型。在不运行程序的情况下执行静态类型检查。在大多数静态类型语言中,编译是在程序时完成的。例如C和Java,
对于静态类型,通常不允许变量改变类型,尽管可能存在将变量转换为不同类型的机制。
让我们看一个静态类型语言的快速示例。请考虑以下Java代码段:
String thing;
thing ="Hello";
第一行声明thing的类型是String,所以后面的赋值也必须指定字符串类型,如果你给thing=2就会出错,但是python就不会出错。
虽然,Python始终是一种动态类型语言。但是,PEP 484引入了类型提示,这使得还可以对Python代码进行静态类型检查。
与大多数其他静态类型语言中的工作方式不同,类型提示本身不会导致Python强制执行类型。顾名思义,键入提示只是建议类型。
鸭子类型
在谈论Python时经常使用的另一个术语是鸭子打字。这个绰号来自短语“如果它像鸭子一样行走,它像鸭子一样嘎嘎叫,那它一定是鸭子”(或其任何变化)。
鸭子类型是一个与动态类型相关的概念,其中对象的类型或类不如它定义的方法重要。使用鸭子类型根本不需要检查类型,而是检查给定方法或属性是否存在。
下面一个例子, 你可在python所有的对象中使用 len() 的魔法函数__len__() 方法:
classTheHobbit: >
...def__len__(self):
...return95022
...
> the_hobbit = TheHobbit()
> len(the_hobbit)
95022
实际len()方法就是下面的这种方法实现的:
deflen(obj):
return obj.__len__()
由此发现,对象也可以像str,list,dict那样使用len方法,只不过需要重新写__len__魔法函数即可。
Hello Types
在本节中,您将看到如何向函数添加类型提示。下面的函数通过添加适当的大写字母和装饰线将文本字符串转换为标题:
defheadline(text, align=True):
if align:
returnf"{text.title()}\n{\'-\' * len(text)}"
else:
returnf"{text.title()} ".center(50,"o")
默认情况下,函数返回与下划线对齐的左侧标题。通过将align标志设置为False,您还可以选择使用o围绕字符串:
"python type checking")) print(headline(
Python Type Checking
--------------------
"python type checking", align=False)) print(headline(
oooooooooooooo Python Type Checking oooooooooooooo
是时候给我们第一个类型提示了!要向函数中添加关于类型的信息,只需如下注释其参数和返回值:
defheadline(text: str, align: bool = True) -> str:
...
text: str 意思是text值类型是str, 类似的, 可选参数 align 指定其类型为bool并给定默认值True. 最后, -> str 表示函数headline() 返回值类型为str。
在代码风格方面,PEP 8建议如下::
对冒号使用常规规则,即冒号前没有空格,冒号后面有一个空格:text:str。
将参数注释与默认值组合时,在=符号周围使用空格:align:bool = True。
defheadline(...) - > str,使用空格围绕。
>>>print(headline("python type checking", align="left"))
Python Type Checking
--------------------
但是如果传入的参数类型不是指定的参数类型,程序不会出现错误,此时可以使用类型检查模块通过提示内容确定是否类型输入正确,如mypy。
你可以通过pip安装
:
$ pip install mypy
将以下代码放在名为headlines.py的文件中:
# headlines.py
defheadline(text: str, align: bool = True) -> str:
if align:
returnf"{text.title()}\n{\'-\' * len(text)}"
else:
returnf"{text.title()} ".center(50,"o")
print(headline("python type checking"))
print(headline("use mypy", align="center"))
然后通过mypy运行上面的文件:
$ mypy headlines.py
headlines.py:10: error: Argument"align" to"headline" has incompatible
type"str"; expected"bool"
根据类型提示,Mypy能够告诉我们我们在第10行使用了错误的类型
这样说明一个问题参数名align不是很好确定参数是bool类型,我们将代码改成下面这样,换一个识别度高的参数名centered。
# headlines.py
defheadline(text: str, centered: bool = False):
ifnot centered:
returnf"{text.title()}\n{\'-\' * len(text)}"
else:
returnf"{text.title()} ".center(50,"o")
print(headline("python type checking"))
print(headline("use mypy", centered=True))
再次运行文件发现没有错误提示,ok。
$ mypy headlines.py
$
然后就可以打印结果了
python headlines.py
PythonType Checking
--------------------
ooooooooooooooooooooUse Mypy oooooooooooooooooooo
第一个标题与左侧对齐,而第二个标题居中。
Pros and Cons
类型提示的增加方便了IDE的代码提示功能,我们看到下面text使用.即可得到str使用的一些方法和熟悉。
类型提示可帮助您构建和维护更清晰的体系结构。编写类型提示的行为迫使您考虑程序中的类型。虽然Python的动态特性是其重要资产之一,但是有意识地依赖于鸭子类型,重载方法或多种返回类型是一件好事。
需要注意的是,类型提示会在启动时带来轻微的损失。如果您需要使用类型模块,那么导入时间可能很长,尤其是在简短的脚本中。
那么,您应该在自己的代码中使用静态类型检查吗?这不是一个全有或全无的问题。幸运的是,Python支持渐进式输入的概念。这意味着您可以逐渐在代码中引入类型。没有类型提示的代码将被静态类型检查器忽略。因此,您可以开始向关键组件添加类型,只要它能为您增加价值,就可以继续。
关于是否向项目添加类型的一些经验法则:
如果您刚开始学习Python,可以安全地等待类型提示,直到您有更多经验。
类型提示在短暂抛出脚本中增加的价值很小。
在其他人使用的库中,尤其是在PyPI上发布的库中,类型提示会增加很多价值。使用库的其他代码需要这些类型提示才能正确地进行类型检查。
在较大的项目中,类型提示可以帮助您理解类型是如何在代码中流动的,强烈建议您这样做。在与他人合作的项目中更是如此。
Bernat Gabor在他的文章《Python中类型提示的状态》中建议,只要值得编写单元测试,就应该使用类型提示。实际上,类型提示在代码中扮演着类似于测试的角色:它们帮助开发人员编写更好的代码。
注解
Python 3.0中引入了注释,最初没有任何特定用途。它们只是将任意表达式与函数参数和返回值相关联的一种方法。
多年以后,PEP 484根据Jukka Lehtosalo博士项目Mypy所做的工作,定义了如何向Python代码添加类型提示。添加类型提示的主要方法是使用注释。随着类型检查变得越来越普遍,这也意味着注释应该主要保留给类型提示。
接下来的章节将解释注释如何在类型提示的上下文中工作。
函数注解
之前我们也提到过函数的注解例子向下面这样:
deffunc(arg: arg_type, optarg: arg_type =default) -> return_type:
...
对于参数,语法是参数:注释,而返回类型使用- >注释进行注释。请注意,注释必须是有效的Python表达式。
以下简单示例向计算圆周长的函数添加注释::
import math
defcircumference(radius: float) -> float:
return2 * math.pi * radius
通调用circumference对象的__annotations__魔法函数可以输出函数的注解信息。
1.23) > circumference(
7.728317927830891
_ > circumference.__annotations_
{\'radius\': <class \'float\'>, \'return\': <class \'float\'>}
有时您可能会对Mypy如何解释您的类型提示感到困惑。对于这些情况,有一些特殊的Mypy表达式:reveal type()和reveal local()。您可以在运行Mypy之前将这些添加到您的代码中,Mypy将报告它所推断的类型。例如,将以下代码保存为reveal.py。
# reveal.py
import math
reveal_type(math.pi)
1 =
2 * math.pi * radius =
reveal_locals()
然后通过mypy运行上面代码
$ mypy reveal.py
reveal.py:4: error: Revealed typeis\'builtins.float\'
reveal.py:8: error: Revealed local types are:
reveal.py:8: error: circumference: builtins.float
reveal.py:8: error: radius: builtins.int
即使没有任何注释,Mypy也正确地推断了内置数学的类型。以及我们的局部变量半径和周长。
注意:以上代码需要通过mypy运行,如果用python运行会报错,另外mypy 版本不低于 0.610
变量注解
有时类型检查器也需要帮助来确定变量的类型。变量注释在PEP 526中定义,并在Python 3.6中引入。语法与函数参数注释相同:
pi: float = 3.142
defcircumference(radius: float) -> float:
return2 * pi * radius
pi被声明为
float类型。
注意:静态类型检查器能够很好地确定3.142是一个浮点数,因此在本例中不需要pi的注释。随着您对Python类型系统的了解越来越多,您将看到更多有关变量注释的示例。.
变量注释存储在模块级__annotations__字典中::
1) > circumference(
6.284
_ > __annotations_
{\'pi\': <class \'float\'>}
即使只是定义变量没有给赋值,也可以通过__annotations__获取其类型。虽然
在python中没有赋值的变量直接输出是错误的。
nothing: str >
> nothing
NameError: name\'nothing\' isnotdefined
_ > __annotations_
{\'nothing\': <class \'str\'>}
类型注解
如上所述,注释是在Python 3中引入的,并且它们没有被反向移植到Python 2.这意味着如果您正在编写需要支持旧版Python的代码,则无法使用注释。
要向函数添加类型注释,您可以执行以下操作:
import math
defcircumference(radius):
# type: (float) -> float
return2 * math.pi * radius
类型注释只是注释,所以它们可以用在任何版本的Python中。
类型注释由类型检查器直接处理,所以不存在__annotations__
字典对象中:
>>>circumference.__annotations__
{}
类型注释必须以type: 字面量开头,并与函数定义位于同一行或下一行。如果您想用几个参数来注释一个函数,您可以用逗号分隔每个类型:
defheadline(text, width=80, fill_char="-"):
# type: (str, int, str) -> str
returnf"{text.title()} ".center(width, fill_char)
print(headline("type comments work", width=40))
您还可以使用自己的注释在单独的行上编写每个参数:
# headlines.py
defheadline(
text,# type: str
width=80,# type: int
fill_char="-",# type: str
):# type: (...) -> str
return f" {text.title()} ".center(width, fill_char)
print(headline("type comments work", width=40))
通过Python和Mypy运行示例:
$ python headlines.py
---------- Type Comments Work ----------
$ mypy headline.py
$
如果传入一个字符串width="full",再次运行mypy会出现一下错误。
$ mypy headline.py
headline.py:10: error: Argument"width" to"headline" has incompatible
type"str"; expected"int"
您还可以向变量添加类型注释。这与您向参数添加类型注释的方式类似:
pi = 3.142# type: float
上面的例子可以检测出pi是float类型。
So, Type Annotations or Type Comments?
所以向自己的代码添加类型提示时,应该使用注释还是类型注释?简而言之:尽可能使用注释,必要时使用类型注释。
注释提供了更清晰的语法,使类型信息更接近您的代码。它们也是官方推荐的写入类型提示的方式,并将在未来进一步开发和适当维护。
类型注释更详细,可能与代码中的其他类型注释冲突,如linter指令。但是,它们可以用在不支持注释的代码库中。
还有一个隐藏选项3:存根文件。稍后,当我们讨论向第三方库添加类型时,您将了解这些。
存根文件可以在任何版本的Python中使用,代价是必须维护第二组文件。通常,如果无法更改原始源代码,则只需使用存根文件。
Playing With Python Types, Part 1
到目前为止,您只在类型提示中使用了str,float和bool等基本类型。但是Python类型系统非常强大,它可以支持多种更复杂的类型。
在本节中,您将了解有关此类型系统的更多信息,同时实现简单的纸牌游戏。您将看到如何指定:
序列和映射的类型,如元组,列表和字典
键入别名,使代码更容易阅读
该函数和方法不返回任何内容
可以是任何类型的对象
在简要介绍了一些类型理论之后,您将看到更多用Python指定类型的方法。您可以在这里找到代码示例:https://github.com/realpython/materials/tree/master/python-type-checking
Example: A Deck of Cards
以下示例显示了一副常规纸牌的实现:
# game.py
import random
SUITS ="♠ ♡ ♢ ♣".split()
RANKS ="2 3 4 5 6 7 8 9 10 J Q K A".split()
defcreate_deck(shuffle=False):
"""Create a new deck of 52 cards"""
deck = [(s, r)for rin RANKS