目录
一、说明
Python为促进清晰可靠的代码而提供的强大工具之一是“类型提示”的概念。你可能想知道,“Python是一种动态类型的语言,那么我为什么要为类型而烦恼呢?"
作为对编码最佳实践感兴趣的数据工程师或 Python 初学者,理解和应用 Python 代码中的类型提示可能是一项真正的资产。
在本文中,我们将更深入地探讨类型提示、它们的应用以及它们在 Python 编程中的优势。由于 Dagster 是一个类型注释框架,我们还将解释如何在数据工程管道中使用类型来提高其可读性并使其不易出错。这就像为你未来的自己和其他可能与你的代码交互的开发人员提供一个映射 - 一个详细说明流入和流出你的函数和类的数据类型的映射。
二、什么是动态类型?
Python是一种动态类型的语言。在 Java 或 C++ 等静态类型语言中,您必须在使用变量之前声明变量的类型。例如,您需要指定变量是整数、浮点数、字符串等。在 Python 中,你可以在运行时之前不考虑数据类型的情况下进行编码——这是使 Python 特别适合初学者的功能之一。
例如,您可以声明一个变量并直接为其赋值,而无需指定其类型,因此称为“动态类型”。Python 解释器在运行时隐式绑定值及其类型。
x = 10 # x is an integer
x = "Hello" # now x is a string
在第一行中,是一个整数。在第二行中,相同变为字符串。由于其动态类型特性,Python 可以无缝地处理这种转换。x
x
但是,这种动态特性也可能导致难以调试的错误,尤其是在大型代码库或复杂的数据处理管道中,其中数据流可能不会立即明显。
Python 3.5 中通过 PEP 484 作为标准库的一部分引入的类型提示允许您指定变量、函数参数或返回值的预期类型。
2.1 为什么要使用类型提示?
虽然动态类型提供了灵活性,但它也为潜在的错误创造了空间。这就是类型提示的用武之地。它们可以显著增强代码可读性并防止与类型相关的错误。
改进了代码可读性:类型提示充当一种文档形式,可帮助开发人员了解函数期望的参数类型及其返回的内容。这种增强的清晰度使代码更具可读性和更易于理解。
错误检测:像“pyright”这样的工具,可以用来静态分析你的Python代码。它根据类型提示检查代码中类型的一致性,并在运行时之前提醒您与类型相关的错误。了解为什么Dagster团队建议完全跳过mypy,只使用pyright。mypy
更好的 IDE 支持:许多集成开发环境 (IDE) 和 linter 可以利用类型提示来提供更好的代码完成、错误检查和重构。
促进大型项目:对于具有多个开发人员的大型项目,类型提示对于理解整个代码库的数据结构和流非常有益。我们发布了有关如何包含和维护公共 Python 项目类型注释的指南。
2.2 局限性
运行时未强制执行: Python 的类型提示不是强制执行的,而只是提示,如果提供的类型与实际值不匹配,Python 解释器不会引发错误。这可能会导致一种误解,即类型提示可以强制实施类型安全,而它们不能强制执行。
过于复杂:对于小型或简单的脚本,类型提示可能看起来有点矫枉过正,并且可能会使本应简单明了的代码复杂化。
不灵活:Python受欢迎的原因之一是它的动态性质和类型提示可以限制这一点。
三、基本类型提示
Python 的模块包含多个函数和类,用于为 Python 代码提供类型提示。下面介绍如何在不同方案中应用类型提示。typing
3.1 声明变量的类型
若要为变量提供类型提示,可以使用冒号符号后跟类型。下面是一个示例::
age: int = 20
name: str = "Alice"
is_active: bool = True
此处提示为整数、字符串和布尔值。age
name
is_active
3.2 函数注释
可以为函数参数和返回值提供类型提示。这有助于其他开发人员了解函数需要哪些类型的参数以及函数返回的类型。
def greet(name: str) -> str: \ return f"Hello, {name}
"
在此示例中,函数期望是一个字符串,并将返回一个字符串。
四、Python 中的内置类型
Python 有几种内置类型。最常用的是:
int
:表示一个整数float
:表示浮点数bool
:表示布尔值(真或假)str
:表示字符串
还有一些复杂的类型,如列表、元组和字典,可用于提供更详细的类型提示,我们将在后面看到这些提示。
您还可以在附录中找到 Python 主要类型的列表。
4.1 原子类型与复合类型
在 Python 中,在类型提示方面,原子类型和复合类型之间存在区别。原子类型(如 、 和 )是简单且不可分割的,它们的类型注释可以直接使用类型本身提供,例如 。int
float
str
str
def my_function(my_string: str) -> int:
return len(my_string)
另一方面,复合类型喜欢 和 由其他类型组成,在 Python 3.9 之前,它们通常需要从类型模块导入特定的定义,例如整数列表。List
Dict
typing.List[int]
from typing import List
def my_function(numbers: List[int]) -> int:
return sum(numbers)
在较新版本的 Python 中,您可以编写而不是 .list[str]
typing.List[int]
五、函数注释
类型提示在合并到函数签名中时特别有用。这不仅允许开发人员了解函数需要什么类型的参数,还可以让他们了解函数将返回什么。
5.1 如何指定函数的参数类型和返回类型
可以使用参数的符号和返回类型的符号来指定参数类型和函数的返回类型。下面是一般语法::
->
def function_name(arg1: type1, arg2: type2, ...) -> return_type:
# function body
在此语法中,、、等是函数参数,、 等是这些参数的类型。 是函数返回的值的类型。arg1
arg2
type1
type2
return_type
5.2 在函数签名中使用类型提示的示例
让我们考虑一个计算矩形面积的函数:
def area_rectangle(length: float, breadth: float) -> float:
return length * breadth
在此函数中,并且预期为浮点数,并且该函数还返回浮点数。如果传递可以转换为浮点数的整数甚至字符串,该函数仍将有效,但类型提示清楚地表明它旨在处理浮点数。length
breadth
另一个示例可以是接受整数列表并将其总和作为整数返回的函数:
def sum_elements(numbers: list[int]) -> int:
return sum(numbers)
在此示例中,参数提示为整数列表,返回类型为整数。numbers
请注意,这些类型提示不会在运行时强制进行类型检查。它们是对开发人员的提示,如果实际类型与指定类型不匹配,Python 不会引发 a。TypeError
六、复杂类型
Python 中的类型模块提供了几个类,可用于提供更复杂的类型提示。以下是一些最常用的类:
6.1 列表、字典、元组、集合
、、 和类可用于分别为列表、字典、元组和集合提供类型提示。可以对它们进行参数化,以提供更详细的类型提示。list
dict
tuple
set
# A list of integers
numbers: list[int] = [1, 2, 3]
# A dictionary with string keys and float values
weights: dict[str, float] = {"apple": 0.182, "banana": 0.120}
# A tuple with an integer and a string
student: tuple[int, str] = (1, "John", "True")
# A set of strings
flags: set[str] = {"apple", "banana", "cherry"}
在这些示例中,提示为整数列表,是具有字符串键和浮点值的字典,是包含整数和字符串的元组,并且是一组字符串。numbers
weights
student
flags
6.2 自选
类型提示可用于指示变量可以是特定类型或 。Optional
None
from typing import Optional
def find_student(student_id: int) -> Optional[dict[str, str]]:
# If the student is found, return a dictionary containing their data
# If the student is not found, return None
6.3 联合
类型提示用于指示变量可以是几种类型之一。例如,如果变量可以是 a 或 ,则可以提供如下类型提示:Union[
str,
int]
from typing import Union
def process(data: Union[str, int]) -> None:
# This function can handle either a string or an integer
在较新版本的 Python 中,您可以使用管道 (|) 运算符来指示可以是多个选项之一的类型,从而替换了对 的需求:Union
def process(data: str | int) -> None:
# This function can handle either a string or an integer
6.4 任何
该类用于指示变量可以是任何类型的变量。这相当于根本不提供类型提示。Any
from typing import Any
def process(data: Any) -> None:
# This function can handle data of any type
模块中的这些工具可以帮助您提供详细的类型提示,使代码更易于理解和调试。typing
但是,请记住,Python 的类型提示是可选的,不会在运行时强制执行。它们旨在作为开发人员的工具,而不是强制类型安全的方法。
七、用户定义的类型
在 Python 中,您可以使用类定义自己的类型,这是创建自定义类型的基本机制。可以在类型提示中使用这些类,就像使用内置类型一样。该模块还提供了用于创建更具体类型的其他工具,包括 和 。typing
Type
NewType
7.1 使用类定义自己的类型
您可以创建一个类并将其用作类型提示。下面是一个示例:
class Student:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def print_student_details(student: Student) -> None:
print(student.name, student.age)
Student
是用户定义的类型,它用作函数中的类型提示。print_student_details
7.2 用于类型提示Type
模块中的类可用于指示变量将是一个类,而不是类的实例。当函数参数预期为类时,例如在工厂函数中,通常使用此方法。Type
typing
<span style="color:#3a3631"><span style="background-color:#faf9f7"><span style="background-color:#3a3631"><span style="color:#dad8d6"><code>from typing import Type
def create_student(cls: Type[Student], name: str, age: int) -> Student:
return cls(name, age)
</code></span></span></span></span>
在此示例中,期望一个类(或 的子类)作为其第一个参数。create_student
Student
Student
7.3 用于创建不同类型NewType
NewType
用于创建不同的类型。当您想要区分两种本来相同的类型时,它很有用。
例如,假设您正在处理程序中的学生 ID 和课程 ID,并且您希望确保不会将它们混淆。两者都表示为整数,因此您可以使用创建两种不同的类型:NewType
from typing import NewType
StudentID = NewType('StudentID', int)
CourseID = NewType('CourseID', int)
def get_student(student_id: StudentID) -> None:
# Fetch student data...
def enroll_in_course(student_id: StudentID, course_id: CourseID) -> None:
# Enroll the student in the course...
尽管 和 都是整数,但它们也被视为不同的类型,不能互换使用。但是,请记住,此检查不是在运行时强制执行的,而是在静态类型检查期间使用诸如 .StudentID
CourseID
mypy
八、泛 型
泛型允许您定义适用于不同类型的函数、类或数据结构。模块中的类和函数用于定义泛型类型。例如,列表是一种通用数据结构,因为它可以包含任何类型的元素。Generic
TypeVar
typing
8.1 类型变量
TypeVar 用于定义类型变量,该变量可以是任何类型,具体类型由客户端代码确定。下面是一个示例:
from typing import TypeVar
T = TypeVar('T')
def first_element(lst: List[T]) -> T:
return lst[0]
这里,是一个类型变量,可以是任何类型。该函数处理任何类型的列表,并返回该类型的元素。的具体类型将由传递给函数的列表决定。T
first_element
T
8.2 通用
Generic
用于定义泛型类。泛型类可以使用多种类型进行初始化,这些类型用于类中的类型提示中。
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, value: T):
self.value = value
def get(self) -> T:
return self.value
这里,是一个适用于任何类型的泛型类。创建 的实例时,可以指定 的类型,该类型将在属性和方法中使用。Box
T
Box
T
value
get
box1 = Box[int](10)
box2 = Box[str]("Hello")
box1
是包含整数的 a,是包含字符串的 a。Box
box2
Box
九、类型检查pyright
像这样的类型检查器是一种用于在 Python 中强制执行类型提示的工具。在Dagster,我们真的很喜欢pyright,因为它比其他替代品更快,例如。pyright
mypy
Python 本身是一种动态类型语言,这意味着类型检查在运行时进行,并且不强制执行类型提示规则。如果尝试执行给定数据类型不支持的操作,Python 将在运行时引发错误。例如,在对象上调用未定义的方法只会在运行时触发错误。
但是,在开发大型或复杂系统时,强制实施类型一致性有助于及早发现潜在的 bug。 执行静态类型检查,这意味着它在实际运行代码之前检查变量、函数参数和返回值的类型。它使用你在代码中提供的类型提示来执行此操作。重要的是要了解它不会执行或运行您的代码;它只是读取和分析它。pyright
pyright
9.1 如何使用类型检查器验证您的类型
要使用 ,您首先需要安装它:pyright
pip install pyright
然后,要检查 Python 文件,请将该文件作为参数运行:pyright
pyright my_file.py
然后,Pyright将分析该文件并报告它发现的任何类型错误。
例如,如果您有一个被注释为str
接收 a 作为参数的函数,并且您传递了一个int
,pyright
将捕获此参数。
9.2 静态与动态类型检查
静态类型检查是基于对程序文本(源代码)的分析来验证程序的类型安全性的过程。静态类型检查在编译时(在程序运行之前)完成。强制静态类型检查的语言包括C++、Java 和 Rust。
另一方面,动态类型检查是在运行时验证程序的类型安全性的过程。动态类型检查在程序运行时进行。使用动态类型检查的语言包括 Python、Ruby 和 JavaScript。
在静态类型检查中,在程序运行之前检查类型,这样可以更轻松地捕获和防止类型错误。这使得程序运行起来更安全,因为大多数与类型相关的错误都是在编译时捕获的。但是,它也要求程序员显式声明所有变量和函数返回值的类型,这可以被视为降低了灵活性。
动态类型检查提供了更大的灵活性,因为您不必显式声明每个变量的类型。但是,这也意味着在运行时可能发生类型错误,这可能会导致程序崩溃。
Python 是一种动态类型语言,但它也支持通过 和 类型提示等工具进行可选的静态类型检查。这为Python程序员提供了独特的灵活性,允许他们选择何时需要静态类型检查的安全性以及何时喜欢动态类型的灵活性。pyright
十、类型提示和文档字符串
正如我们所讨论的,类型提示指示变量、函数参数和返回值的类型。它们可以帮助其他开发人员了解您的函数需要哪些类型的数据以及它将返回什么。
另一方面,文档字符串用于提供函数、类或模块的功能描述。文档字符串可以包含对函数用途、参数、返回值以及可能引发的任何异常的描述。
下面是如何同时使用类型提示和文档字符串的示例:
def filter_and_sort_products(products: list[dict[str, int]], attribute: str, min_value: int) -> list[dict[str, int]]:
"""
Filters a list of products by a given attribute and minimum value, and then sorts the filtered products by the attribute.
Args:
products (list[dict[str, int]]): A list of products represented as dictionaries.
attribute (str): The attribute to filter and sort by.
min_value (int): The minimum acceptable value of the specified attribute.
Returns:
list[dict[str, int]]: A list of filtered and sorted products.
Raises:
KeyError: If the specified attribute is not found in any product.
Examples:
>>> products = [{"name": "Apple", "price": 10}, {"name": "Banana", "price": 5}]
>>> filter_and_sort_products(products, "price", 6)
[{"name": "Apple", "price": 10}]
"""
filtered_products = [product for product in products if product[attribute] >= min_value]
return sorted(filtered_products, key=lambda x: x[attribute])
在这里,函数签名显示该函数采用表示产品的字典列表、表示属性的字符串和表示最小值的整数。它返回经过筛选和排序的词典的列表。
doc 字符串解释了函数的用途、参数、返回值、可能的异常(例如,如果给定属性不存在),并包括如何调用函数的示例。KeyError
类型提示和文档字符串的这种组合可以大大提高代码的可读性和可维护性。
十一、结论
在 Python 编程最佳实践的基础上,我们研究了类型提示如何提高代码的可读性和可维护性。
如果您有任何问题或需要进一步澄清,请随时加入 Dagster Slack 并向社区寻求帮助。感谢您的阅读!