PYTHON 进阶学习:typing模块

内容分享2小时前发布 iiiyw_zz
0 0 0

Python 作为一门动态编程语言,在定义变量时无需显式声明变量类型,能够为变量赋予各类不同的数据类型。这种特性乍看之下,赋予了编程极大的便利性与灵活性,不过,随着代码规模的持续扩张,它会导致代码的可读性显著降低,给代码的后期维护工作带来了极大的挑战。

为应对这一难题,自 Python 3.5 版本起,官方引入了 typing 模块,旨在为运行时提供类型提示支持。目前,常见的静态类型检查工具包括 mypy、pyright 等。接下来,我们将从多个维度深入剖析 typing 模块。

mypy教程

当下,mypy 已然成为 Python 领域中占据主流地位的类型检查工具。可以通过 pip 命令来安装该工具:

pip install mypy

使用方式:

mypy xxx.py

类型别名

类型别名的定义借助 type 语句来实现,此过程会创建一个 TypeAliasType。下面通过具体示例对其加以阐释:

type Vector = list[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

integer_vector = [1, 2, 3]
float_vector = [1.0, -1.1, 1.1]
# 通过类型检查;浮点数列表是合格的 Vector。
scale(2.0, float_vector)
# 下面的会提示类型错误,由于整数列表不是 Vector。
scale(2.0, integer_vector)

# 执行mypy,会报如下错误
# typing_test.py:11: error: Argument 2 to "scale" has incompatible type "list[int]"; expected "list[float]"  [arg-type]
# typing_test.py:11: note: "list" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
# typing_test.py:11: note: Consider using "Sequence" instead, which is covariant

NewType

NewType 具备基于现有类型创建全新类型的能力。借助 NewType 所定义的新类型变量,能够执行基础类型所支持的全部操作,诸如加、减、乘、除等算术运算。

from typing import NewType

ID = NewType('ID', int)

a = ID(123)
b = ID(456)

def set_id(id: ID) -> str:
    ...

# 可以执行int的所有操作,但返回结果不是ID,是int
c = a + b

# 校验参数类型通过
set_id(a)
# 校验参数类型失败
set_id(c)

泛型

泛型,简单来说,就是一种在定义函数、类或数据结构时不预先指定具体类型,而是在使用时再确定类型的编程方式。在 Python 中,泛型的引入主要是为了解决代码复用性和类型提示的问题。当我们编写一些通用的函数或类时,可能会处理多种不同类型的数据,如果为每种数据类型都编写一套代码,会导致代码冗余且难以维护。而泛型可以让我们编写一套通用的代码,适用于多种类型的数据。

使用 typing 模块实现泛型

Python 的 typing 模块为我们提供了实现泛型的工具。下面我们通过几个具体的例子来详细说明。

泛型函数

泛型函数是指可以处理多种类型数据的函数。我们可以使用 TypeVar 来定义泛型类型变量。

from typing import TypeVar, List

# 定义一个泛型类型变量 T
T = TypeVar('T')

def get_first_element(lst: List[T]) -> T:
    """
    该函数接受一个列表,并返回列表的第一个元素。
    由于使用了泛型类型变量 T,该函数可以处理任何类型的列表。
    """
    if lst:
        return lst[0]
    return None

# 使用示例
int_list = [1, 2, 3]
str_list = ['apple', 'banana', 'cherry']

first_int = get_first_element(int_list)
first_str = get_first_element(str_list)

print(f"First element of int_list: {first_int}")
print(f"First element of str_list: {first_str}")

在上述代码中,我们定义了一个泛型类型变量 T,并将其用于函数 get_first_element 的参数和返回值类型提示。这样,该函数就可以处理任何类型的列表,提高了代码的复用性。

泛型类

泛型类是指可以处理多种类型数据的类。同样,我们可以使用 TypeVar 来定义泛型类型变量。

from typing import TypeVar

# 定义一个泛型类型变量 T
T = TypeVar('T')

class Box:
    def __init__(self, item: T):
        self.item = item

    def get_item(self) -> T:
        return self.item

# 使用示例
int_box = Box(10)
str_box = Box('Hello')

print(f"Item in int_box: {int_box.get_item()}")
print(f"Item in str_box: {str_box.get_item()}")

在这个例子中,我们定义了一个泛型类 Box,它可以存储任何类型的对象。通过使用泛型类型变量 T,我们可以在创建 Box 实例时指定具体的类型。

有界泛型

有时候,我们希望泛型类型变量只能是某些特定类型或其子类型。这时可以使用有界泛型。

from typing import TypeVar, Union

# 定义一个有界泛型类型变量,T 必须是 int 或 float 类型
T = TypeVar('T', int, float)

def add(a: T, b: T) -> T:
    return a + b

# 使用示例
result_int = add(1, 2)
result_float = add(1.5, 2.5)

print(f"Result of int addition: {result_int}")
print(f"Result of float addition: {result_float}")

在上述代码中,我们定义了一个有界泛型类型变量 T,它只能是 int 或 float 类型。这样,函数 add 就只能处理这两种类型的数据。

类对象的类型

Python中,万物皆为对象,类本身也是对象。在默认情况下我们参数接收的是类的实例,但是许多情况下我们会接受类本身作为传参,这时我们就需要区分类对象和类实例,下面我们通过示例来详细说明:

class User:
    def __init__(self, name: str):
        self.name = name

    def greet(self):
        return f"Hello, {self.name}!"

# 函数接收类实例对象
def test(user: User):
    print(user.greet())

user = User("kylin")
test(user)

上面的示例是默认的标注方式,标注参数表明函数接收User类型的实例对象,如果需要接收User类对象,我们应该通过type[C] (或已被弃用的 typing.Type[C]))来进行标注

# 函数接收类对象
def test_class(user_class: type[User]):
    user_instance = user_class("kylin class")
    print(user_instance.greet())

test_class(User)

Any类型

在typing 模块里,Any 类型是一个特殊且常用的类型注解工具。它在为代码带来灵活性的同时,也伴随着必定的风险。下面我们就来深入探讨 Any 类型的特点、应用场景以及潜在问题。

认识 Any 类型

Any 是 typing 模块中定义的一种特殊类型。从本质上来说,它是一种通用类型,代表任意类型。当我们使用 Any 作为类型注解时,意味着该变量可以是任何类型的对象,无论是内置类型(如 int、str、list 等),还是自定义类的实例。

from typing import Any

def print_any(value: Any) -> None:
    print(value)

在上述代码中,print_any 函数的参数 value 被注解为 Any 类型,这表明该函数可以接受任意类型的参数,然后将其打印输出。

Any 类型的应用场景

在处理一些动态生成代码或者与外部系统交互时,由于无法提前确定数据的具体类型,使用 Any 类型就超级合适。例如,在编写一个通用的数据解析函数时,输入的数据可能是 JSON、XML 或者其他格式,数据的类型和结构具有不确定性。

from typing import Any

def parse_data(data: Any) -> Any:
    # 这里可以根据不同的数据类型进行不同的处理
    if isinstance(data, dict):
        return data.get('key')
    elif isinstance(data, list):
        return data[0] if data else None
    return data

在定义dict时,由于字段值类型的不明确,可以使用Any进行标注

from typing import Any

data: dict[str, Any] = {}

旧代码的类型注解迁移

当对旧的 Python 代码进行类型注解添加时,有些函数或变量的类型很难在短期内确定。此时,可以先使用 Any 类型进行临时注解,后续再逐步完善。

from typing import Any

# 旧代码中的函数
def old_function(arg):
    return arg * 2

# 暂时使用 Any 进行类型注解
def old_function_annotated(arg: Any) -> Any:
    return arg * 2

使用 Any 类型的潜在问题

Any 类型绕过了 Python 类型检查工具(如 mypy)的类型检查。由于它表明任意类型,所以类型检查工具不会对使用 Any 注解的变量或函数进行严格的类型验证。这可能导致在代码中引入一些潜在的类型错误,而这些错误在开发阶段难以被发现。

from typing import Any

def add_numbers(a: Any, b: Any) -> Any:
    return a + b

result = add_numbers("hello", 1)  # 这里不会有类型检查错误,但运行时会出错

过度使用 Any 类型会使代码的类型信息变得模糊,其他开发者在阅读和理解代码时会面临困难。代码的类型注解本应是一种文档,协助开发者快速了解变量和函数的用途和预期输入输出,但 Any 类型的滥用会破坏这种文档的作用。

总结

typing 模块并非一种具有强制约束力的机制。在程序的实际运行过程里,我们完全能够不依照所标注的类型进行变量赋值操作。不过,这一模块的存在却有着不可小觑的重大意义。

它的主要价值体目前提升代码的可读性方面。通过清晰明确的类型标注,代码就如同拥有了一份详细的说明书,即使是后续接手项目的开发者,也能迅速理解代码的逻辑和各个变量、函数的用途。这对于代码的后期维护和重构工作而言,无疑是极大的助力。

此外,类型提示能够与集成开发环境(IDE)实现更为紧密的协作。它如同精准的导航,协助 IDE 为开发者提供更为准确的代码补全功能。当我们在编写代码时,IDE 可以根据类型提示,准确地预测我们可能需要的代码片段,大大提高了编码效率。而且,类型提示也使得 IDE 的错误检查功能更加敏锐,能够及时发现潜在的类型不匹配等问题,让我们在开发过程中少走弯路,确保代码的质量和稳定性。

© 版权声明

相关文章

暂无评论

none
暂无评论...