Python类型模块- 有效使用类型检查器

自Python 3.5版本开始引入的Python的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

既然我们已经满足了先决条件,让我们试着使用一些模块的功能。


类型提示/类型注解

关于函数

我们可以使用注解来指定函数的返回类型和参数的类型。

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 3.6以来,我们还可以标注变量的类型,明确指出类型。但是如果您希望在函数返回之前变量的类型发生改变,这并非强制性要求。

# Annotates 'radius' to be a float
radius: float = 1.5

# We can annotate a variable without assigning a value!
sample: int

# Annotates 'area' to return a float
def area(r: float) -> float:
    return 3.1415 * r * r


print(area(radius))

# Print all annotations of the function using
# the '__annotations__' dictionary
print('Dictionary of Annotations for area():', area.__annotations__)

我的mypy输出结果是:

vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py
Success: no issues found in 1 source file
7.068375
Dictionary of Annotations for area(): {'r': <class 'float'>, 'return': <class 'float'>}

在使用mypy时,推荐的方式是在使用类型检查器之前先提供类型注解。


类型别名

typing模块提供了类型别名(Type Aliases),通过为别名指定一个类型进行定义。

from typing import List

# Vector is a list of float values
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

# Create an alias called 'ContactDict'
ContactDict = Dict[str, str]

def check_if_valid(contacts: ContactDict) -> bool:
    for name, email in contacts.items():
        # Check if name and email are strings
        if (not isinstance(name, str)) or (not isinstance(email, str)):
            return False
        # Check for email 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()创建用户自定义数据类型

我们可以使用NewType()函数来创建新的用户定义类型。

from typing import NewType

# Create a new user type called 'StudentID' that consists of
# an integer
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)

静态类型检查器将会将新类型视为原始类型的子类。这在帮助捕捉逻辑错误方面非常有用。

from typing import NewType

# Create a new user type called 'StudentID'
StudentID = NewType('StudentID', int)

def get_student_name(stud_id: StudentID) -> str:
    return str(input(f'Enter username for ID #{stud_id}:\n'))

stud_a = get_student_name(StudentID(100))
print(stud_a)

# This is incorrect!!
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)

任意类型

这是一种特殊类型,在我的情况下,它通知静态类型检查器(例如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

# A static type checker will treat the above
# as having the same signature as:
def foo(bar: Any) -> Any:
    return bar

因此,您可以使用“Any”将静态类型和动态类型的代码混合使用。


结论

在本文中,我们了解了Python类型模块的相关知识,在类型检查的环境中非常实用。它能够让外部类型检查器(如mypy)能够准确报告任何错误。

这为我们提供了一种在设计上是动态类型的Python中写静态类型代码的方式!


参考资料

  • Python Documentation for the typing module (This contains extensive detail on more methods in this module, and I recommend this as a secondary reference)
  • StackOverflow question on type hints (This provides a very good discussion on the topic. I highly recommend you to read this topic as well!)

发表回复 0

Your email address will not be published. Required fields are marked *


广告
将在 10 秒后关闭
bannerAds