Python类型模块:高效利用类型检查器提升代码质量
这是文章《Python类型模块 – 有效使用类型检查器》的第1部分(共5部分)。
自Python 3.5版本开始引入的typing
模块,旨在提供一种类型提示的方式,以辅助静态类型检查器和代码分析工具更准确地预测潜在错误。
由于Python在运行时才确定对象的类型,这有时会给开发者理解代码执行过程带来困扰。
根据StackOverflow上的相关讨论,即使是PyCharm IDE这类外部类型检查器,其错误预测的准确率也仅为50%左右,远未达到最佳效果。
Python通过引入类型提示(或称类型注解)来缓解这一问题,帮助外部类型检查器识别潜在错误。这为程序员提供了一种在“编译时”暗示对象类型的好方法,从而确保类型检查器正常工作。
这不仅使Python代码对其他阅读者来说更易读,也提升了代码的健壮性!
请注意:类型提示本身并不会在运行时进行实际的类型检查。如果实际返回的对象与提示的类型不一致,并不会引发编译错误。这正是我们需要借助外部类型检查器(如mypy)来识别类型错误的原因。
建议的先决条件
为了有效利用typing
模块,强烈建议您使用外部类型检查器/语法检查器来执行静态类型匹配。在Python生态系统中,mypy是最广泛使用的类型检查器之一。因此,在阅读本文的其余部分之前,我建议您先安装它。
我们已经介绍了Python中类型检查的基础知识。您可以先阅读这篇文章。
在本文中,我们将使用mypy作为静态类型检查器,其安装方式如下:
pip3 install mypy
您可以运行mypy来检查任何Python文件的类型匹配情况。这就像您在“编译”Python代码一样。
mypy program.py
在调试并解决错误之后,您可以使用以下方式正常运行程序:
python program.py
既然我们已经满足了先决条件,现在让我们尝试使用typing
模块的一些功能。
类型提示/类型注解
关于函数
我们可以使用注解来指定函数的返回类型和参数的类型。
def print_list(a: list) -> None:
print(a)
这会告知类型检查器(我这里指的是mypy):我们有一个名为print_list()
的函数,它接受一个列表作为参数并返回None
。
def print_list(a: list) -> None:
print(a)
print_list([1, 2, 3])
print_list(1)
让我们先在我们的类型检查器mypy上运行这段代码。
vijay@JournalDev:~ $ mypy printlist.py
printlist.py:5: error: Argument 1 to "print_list" has incompatible type "int"; expected "List[Any]"
Found 1 error in 1 file (checked 1 source file)
果然,我们遇到了一个错误;因为第五行的参数是整数而不是列表,这与我们定义的类型提示不符。
关于变量
这是文章《Python类型模块 – 有效使用类型检查器》的第2部分(共5部分)。
自Python 3.6版本起,我们能够通过类型注解明确指定变量的类型。然而,如果变量的类型在函数返回前可能发生变化,则不强制要求进行类型标注。
# 将 'radius' 标注为浮点型
radius: float = 1.5
# 我们可以不赋值就标注变量类型!
sample: int
# 标注函数 'area' 返回浮点型
def area(r: float) -> float:
return 3.1415 * r * r
print(area(radius))
# 使用 '__annotations__' 字典打印函数的所有注解
print('area() 函数的注解字典:', area.__annotations__)
我的 mypy 输出结果是:
vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py
Success: no issues found in 1 source file
7.068375
area() 函数的注解字典: {'r': <class 'float'>, 'return': <class 'float'>}
在使用 mypy 进行类型检查时,推荐的做法是在运行类型检查器之前提供类型注解。
类型别名
这是文章《Python类型模块 – 有效使用类型检查器》的第3部分(共5部分)。
typing
模块提供了类型别名(Type Aliases),通过为别名指定一个类型进行定义。
from typing import List
# Vector 是一个浮点数值列表
Vector = List[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
a = scale(scalar=2.0, vector=[1.0, 2.0, 3.0])
print(a)
输出
vijay@JournalDev: ~ $ mypy vector_scale.py && python vector_scale.py
Success: no issues found in 1 source file
[2.0, 4.0, 6.0]
在上面的代码片段中,"Vector"
是一个别名,代表了一个浮点数值列表。我们可以对别名进行类型提示,这正是上述程序所做的。
以下是所有可接受的替代名称的完整列表。
让我们再看一个例子,它将检查字典中的每一个键值对,并验证它们是否符合“名称:电子邮件”的格式。
from typing import Dict
import re
# 创建一个名为 'ContactDict' 的别名
ContactDict = Dict[str, str]
def check_if_valid(contacts: ContactDict) -> bool:
for name, email in contacts.items():
# 检查名称和电子邮件是否为字符串
if (not isinstance(name, str)) or (not isinstance(email, str)):
return False
# 检查电子邮件格式是否为 xxx@yyy.zzz
if not re.match(r"[a-zA-Z0-9\._\+-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+$", email):
return False
return True
print(check_if_valid({'vijay': 'vijay@sample.com'}))
print(check_if_valid({'vijay': 'vijay@sample.com', 123: 'wrong@name.com'}))
从 mypy 的输出结果中得出:
vijay@JournalDev:~ $ mypy validcontacts.py
validcontacts.py:19: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "str"
Found 1 error in 1 file (checked 1 source file)
在这里,我们在 mypy 中得到了一个静态编译时的错误,因为我们第二个字典的键(name
参数)是一个整数(123
),而不是预期的字符串。因此,类型别名是 mypy 强制执行准确类型检查的另一种有效方式。
使用 NewType()
创建用户自定义数据类型
这是文章《Python类型模块 – 有效使用类型检查器》的第4部分(共5部分)。
我们可以使用NewType()
函数来创建新的用户定义类型。
from typing import NewType
# 创建一个名为'StudentID'的新用户类型,它由一个整数组成
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)
静态类型检查器会将新类型视为原始类型的子类。这在帮助捕捉逻辑错误方面非常有用。
from typing import NewType
# 创建一个名为'StudentID'的新用户类型
StudentID = NewType('StudentID', int)
def get_student_name(stud_id: StudentID) -> str:
return str(input(f'请输入ID #{stud_id}的用户名:\n'))
stud_a = get_student_name(StudentID(100))
print(stud_a)
# 这是错误的!!
stud_b = get_student_name(-1)
print(stud_b)
我的py文件输出:
vijay@JournalDev:~ $ mypy studentnames.py
studentnames.py:13: error: Argument 1 to "get_student_name" has incompatible type "int"; expected "StudentID"
Found 1 error in 1 file (checked 1 source file)
任意类型
这是文章《Python类型模块 – 有效使用类型检查器》的第5部分(共5部分)。
内容片段:Any
是一种特殊类型。在我看来,它会通知静态类型检查器(例如 Mypy),任何类型都与此关键字兼容。
考虑我们原来的 print_list()
函数,现在可以接受任意类型的参数。
from typing import Any
def print_list(a: Any) -> None:
print(a)
print_list([1, 2, 3])
print_list(1)
现在,当我们运行 Mypy 时将不再出现错误。
vijay@JournalDev:~ $ mypy printlist.py && python printlist.py
Success: no issues found in 1 source file
[1, 2, 3]
1
所有没有返回类型或参数类型的函数将默认使用 Any
。
def foo(bar):
return bar
# 静态类型检查器会将其视为与以下签名相同:
def foo(bar: Any) -> Any:
return bar
因此,您可以使用 Any
将静态类型和动态类型的代码混合使用。
结论
在本文中,我们了解了 Python 类型模块的相关知识,这在类型检查的环境中非常实用。它能够让外部类型检查器(如 Mypy)准确报告任何错误。
这为我们提供了一种在设计上是动态类型的 Python 中编写静态类型代码的方式!
参考资料
- Python 类型模块文档(这包含了该模块中更多方法的详细信息,我推荐将其作为辅助参考资料)
- StackOverflow 上关于类型提示的问题(这提供了非常好的讨论,我强烈建议您也阅读此主题!)