Python 是一种动态类型的语言。我们不需要明确提及声明的变量或函数的数据类型。Python 解释器在运行时根据变量当时的值为其分配类型。我们也有静态类型的语言,如Java、C或C++,我们需要在声明时声明变量的类型,而变量的类型在编译时是已知的。
从Python 3.5开始,在PEP 484和PEP 483中引入了一种叫做类型提示的东西。这种对Python语言的补充有助于结构化我们的代码,使其感觉更像一种静态类型的语言。这有助于避免错误,但同时也使代码更加冗长。
然而,Python运行时并不强制执行函数和变量类型注释。它们可以被第三方工具使用,如类型检查器、IDE、linters等。
类型检查、类型提示和代码编译
起初,我们有一些外部的第三方库,例如静态类型检查器如**mypy**
开始做类型提示,很多来自 mypy 的想法实际上被带入了规范的 Python 本身,并直接集成到 Python 中。
现在,类型提示的问题是,它并不修改Python本身的运行方式。类型提示确实与代码的其他部分一起被编译,但它们并不影响Python如何执行你的代码。
让我们通过一个例子,通过给一个函数分配类型提示来了解一下情况。
代码
def multiply(num1: int, num2: int):
return num1 * num2
print(multiply(5, 7)) # 35
print(multiply(5, "Hi")) # HiHiHiHiHi
解释一下
在上面声明的函数中,我们正在给参数分配内置的数据类型。这是一个很好的普通函数,但这里的语法有点不同。我们可以注意到,参数上有一个分号,上面有一个分配给它们的数据类型 (num1: int, num2: int)
这个函数接收两个参数 **num1
**和**num2**
,这就是 Python 在运行代码时看到的情况。它期待着两个变量。即使我们不放任何类型的提示,Python也会很好,我们指定说 **num1
**和 num2
应该是整数。
所以根据它,我们应该向我们的代码传递两个整数值,这样就可以了。然而,如果我们试图传递一个整数和一个字符串呢?
类型提示告诉我们要传入 **int**
值,但我们却在传递一个 **str**
.当我们试图运行这段代码时,它运行得很好,没有任何问题。如果外面的类型提示中存在有效的数据类型,如**int, str, dict,**
等等。
为什么要使用类型提示呢?
在上面的例子中,我们看到,即使我们向它传递一个字符串值,代码也能正常运行。Python在将一个 **int**
与 str
.然而,有一些非常好的理由来使用类型提示,即使 Python 忽略它们。
- 其中一点是,它可以帮助IDE显示上下文敏感的帮助信息,如不仅显示函数参数,还显示预期类型是什么。
- 类型提示经常被用于代码文档。有多个自动代码文档生成器在生成文档时使用了类型提示,例如,如果我们自己编写有很多函数和类的代码库,也包括注释。
- 即使Python根本不使用类型提示,它也可以帮助我们在编写代码时利用它来使用更多的声明性方法,也可以使用外部库来提供运行时验证。
使用类型检查器
有几个Python的类型检查器。其中之一是mypy。
让我们使用之前运行的同样的代码,使用an **int**
和 str
.我们将使用静态类型检查器 **mypy
**并看看 Python 对我们的代码有什么看法。
- 安装
mypy
pip install mypy
- 使用类型提示的代码,在运行代码时使用类型检查器
代码
def multiply(num1: int, num2: int):
return num1 * num2
print(multiply(5, "Hi"))
在终端运行带有类型检查器前缀的文件:
mypy type-hints.py
输出:
type-hints.py:9: error: Argument 2 to "multiply" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)
解释:
当我们用类型检查器运行我们的文件时,现在Python 解释器对我们的代码有了问题。预期的参数是一个 int
数据类型,而我们在其中一个参数中传递了一个字符串。类型检查器跟踪了这个错误,并在我们的输出中显示出来。该 **mypy**
类型检查器帮助我们解决了代码中的问题。
Python中类型提示的更多例子
在上面的例子中,我们在提示的时候使用了int
和str
类型。同样,其他数据类型也可以用于类型提示。我们也可以在一个函数中为**return
类型**声明一个类型提示。
让我们浏览一下代码,看看一些例子。
例子:1
def some_function(a: int, b: float, c: dict, d: bool = False):
pass
解释一下:
这里我们对参数的不同数据类型进行了类型提示。注意,如果没有提供参数,我们也可以给我们的参数分配默认值。
例子: 2
def person(name: str, age: int) -> str:
return f"{name} is a {age} year old programmer"
解释:
在上面的代码中,返回类型也被声明了。当我们试图使用像 mypy 这样的类型检查器来运行这段代码时,Python 将不会出现问题,因为我们有一个字符串作为返回类型,这与提供的类型提示相匹配。
例子3
def other_function(name: str, age: int) -> None:
return f"{name} is a {age} year old programmer"
解释一下:
这段代码的返回类型为 None
.当我们试图使用 **mypy**
类型检查器运行这段代码时,Python 会引发一个异常,因为它所期望的是一个 **return**
类型为 None
,而这段代码返回的是一个字符串。
例子4
my_list: list = ["apple", "orange", "mango"]
my_tuple: tuple = ("Hello", "Friend")
my_dictionary: dict = {"name": "Peter", "age": 30}
my_set: set = {1, 4, 6, 7}
解释:
上面的代码显示了类型提示,通常被称为变量注解。 就像我们在上面的例子中为我们的函数提供了类型提示一样,即使是变量也可以包含类似的信息,并有助于使代码更具有声明性和文档性。
异常 typing
模块
很多时候,我们有更高级或更复杂的类型,必须作为一个参数传递给函数。Python 有一个内置的 **typing**
模块,使我们能够编写这种类型的注释变量,使代码更有文件性。我们必须将类型化模块导入我们的文件,然后使用那些函数。这些包括数据结构,如List、Dictionary、Set和Tuple。
让我们看看这段代码以及作为解释的注释,以获得一个概览:
from typing import List, Dict, Set, Tuple, Union
# Declaring a nested list with annotations
my_new_list: List[List[int]] = [[1, 2, 3], [4, 5]]
# Declaring a dictionary with keys as strings and values as integers
my_new_dict: Dict[str, int] = {"age": 34, "languages": 2}
# Declaring a set with only string values
my_new_set: Set[str] = {"apple", "mango"}
# Declaring a tuple with exactly 3 parameters with string values only
my_new_tuple: Tuple[str, str, str] = ("Hello", "There", "Friend")
# Declaring a list that may hold integers, floats, and strings all at once
my_union_list: List[Union[int, float, str]] = [12, "notebook", 34.56, "pencil", 78]
'''
Since Python 3.10.x we can use the pipe | character
instead of importing typing Module for using Union
'''
def func(a: str | int | float, b: int) -> str | int | float:
return a * b
总结
在Python中还有许多其他的方法来利用类型提示。使用类型并不影响代码的性能,我们也没有得到任何额外的功能。然而,使用类型提示为我们的代码提供了健壮性,并为以后阅读代码的人提供了文档。
这当然有助于避免引入难以发现的错误。在当前的技术方案中,在编写代码时使用类型正在变得流行,Python也在遵循这一模式,为我们提供了易于使用的功能。欲了解更多信息,请参考官方文档。