Skip to content

使用 Typing 模块提高代码可维护性

Type Hints 是 Python 3.5 引入的一项功能,它允许开发者在函数参数、返回值、变量等地方添加类型提示信息。这并不是强制性的类型检查,而是一种在代码中提供额外信息的机制,用于静态分析、文档生成和提高代码可读性

  • 函数参数和返回值的类型提示:

    python
    def add(x: int, y: int) -> int:
        return x + y

    在这个例子中,x: int 表示参数 x 的类型是整数,y: int 表示参数 y 的类型是整数,-> int 表示函数返回值的类型是整数。

  • 变量的类型提示:

    python
    total: int = 0

    这里的 total: int 表示变量 total 的类型是整数。

  • 类型注解的灵活性:

    python
    def greet(name: str, age: Optional[int] = None) -> str:
        return f"Hello, {name}. You are {age} years old."
    
    
    # 使用类型提示
    result: str = greet("Alice", 25)
  • 泛型(Generics):

    python
    from typing import List, Tuple
    
    
    def process_data(data: List[Tuple[str, int]]) -> List[str]:
        return [f"{name}: {age}" for name, age in data]

    在这个例子中,List[Tuple[str, int]] 表示参数 data 是一个包含元组的列表,每个元组包含一个字符串和一个整数。

  • 使用类型别名(Type Aliases):

    python
    from typing import List
    
    CustomerID = str
    OrderID = str
    
    def process_orders(
        customer_ids: List[CustomerID], 
        order_ids: List[OrderID]
    ) -> None:
        # 处理订单逻辑
        pass

    CustomerIDOrderID 是类型别名,可以提高代码的可读性。尽管类型提示不会强制执行类型检查,但它们为开发者和工具提供了更多信息,使得代码更易于理解、维护和分析。类型提示通常由 IDE、静态分析工具和类型检查器(如 mypy)用于提供更好的开发体验和更早地发现潜在的错误。

typing 模块介绍

typing 模块的主要作用是为 Python 引入类型提示,它允许开发者在代码中提供关于变量、函数参数、函数返回值等的类型信息。 这种类型提示并不会影响程序的实际执行,但它为开发者和工具提供了更多的上下文信息,而主要作用有以下几个:

  1. 代码可读性:类型提示可以让代码更加清晰和易读。通过阅读代码,开发者可以更容易地理解变量的类型、函数的参数和返回值,从而更好地理解代码的意图。
  2. 静态类型检查:使用类型提示后,可以使用静态类型检查工具(例如 mypy)在开发阶段进行类型检查,捕获一些潜在的类型错误。这有助于减少在运行时由于类型问题导致的错误。
  3. 文档生成:类型提示可以被用来生成文档,自动文档生成工具(如 Sphinx)能够根据类型提示自动生成文档,进一步提高文档的准确性和可维护性。
  4. 提高开发工具的智能提示:集成开发环境(IDE)可以利用类型提示提供更准确的代码补全和智能提示,使开发者更加高效。
  5. 功能增强:用于增强 Type Hints 的功能,使得类型提示更加灵活和表达力更强。typing 模块中的类型工具可以用于构建更复杂的类型结构

注意:typing 库的使用并不会像 C++ 与 Java 一样,不匹配类型无法运行,它仅仅只是用于一种指明性的功能

安装 typing 与 typing_extensions

shell
pip install typing
pip install typing_extensions	# 用于支持一些当前 Python 版本可能不支持的特性

内建类型与类型注解

内建类型

Python 默认拥有的几大基本数据类型

数据类型描述示例
int(整数)用于表示整数。x = 5
float(浮点数)用于表示带有小数点的数字。y = 3.14
bool(布尔值)用于表示真(True)或假(False)的值,常用于条件判断。is_valid = True
str(字符串)用于表示文本。message = "Hello, World!"
list(列表)用于表示可变序列,可以包含不同类型的元素。numbers = [1, 2, 3]
tuple(元组)用于表示不可变序列,类似于列表但不能被修改。coordinates = (x, y)
set(集合)用于表示无序、唯一的元素集合。unique_numbers = {1, 2, 3}
dict(字典)用于表示键值对映射。person = {'name': 'John', 'age': 30}
None表示空值或缺失值。result = None

注意:在 python3.9 之后,listdict 均支持泛型类型:list[int] 即整数列表,dict[str, str] 表示以字符串为键值,字符串为值的字典

类型注解

通过 typing 模块引入内容

基本类型:

类型描述
int整数类型
float浮点数类型
bool布尔类型
str字符串类型

容器类型:

类型描述
List列表类型
Tuple元组类型
Dict字典类型
Set集合类型

特殊类型:

类型描述
Any表示任意类型,相当于取消类型检查
Union表示多种可能的类型

Callable 类型:

类型描述
Callable表示一个可调用对象的类型,可以指定函数的输入参数和返回值的类型

Generics:

类型描述
TypeVar定义一个类型变量,用于表示不确定的类型
Generic用于创建泛型类型,可以在类或函数中使用泛型

函数注解:

类型描述
Type表示一个类型
Optional表示一个可选的类型
AnyStr表示字符串的抽象类型,可以是 str 或 bytes

类型别名:

类型描述
NewType创建一个新的类型,用于提高类型的可读性

注意:在 python3.9 之前的版本不能够使用 dict 与 list 内建类型,只能导入使用 typing 库的注解

基础使用

变量注解

编写一段简单的代码:

python
# 可以使用的基本类型来声明变量的类型,如 int,float,bool,str 等
name: str = 'zhengxinonly'
age: int = 18
height: float = 1.75
active: bool = True
blog: str = 'docs.zhengxinonly.com'

numbers_list: list[int] = [1, 2, 3, 4, 5]
numbers_tuple: tuple[int, float, str] = (1, 1.0, "1")
numbers_dict: dict[str, int] = {"one": 1, "two": 2, "three": 3}
numbers_set: set[str] = {"one", "two", "three"}

旧版 Python 解释器也可以使用以下代码

python
from typing import List, Tuple, Dict, Set

numbers_list: List[int] = [1, 2, 3, 4, 5]
numbers_tuple: Tuple[int, float, str] = (1, 1.0, "1")
numbers_dict: Dict[str, int] = {"one": 1, "two": 2, "three": 3}
numbers_set: Set[str] = {"one", "two", "three"}

参数和函数添加注解

python
def add(number1, number2):
    number3 = number1 + number2
    return number3


def add_annotation(number1: int, number2: int) -> int:
    number3: int = number1 + number2
    return number3


add(1, '2')
add_annotation('1', '2')

print(help(add))
"""
Help on function add1 in module __main__:
"""

print(help(add_annotation))
"""
Help on function add_annotation in module __main__:

add_annotation(number1: int, number2: int) -> int
"""

Any 类型提示

python
from typing import Any


def add(x: Any, y: Any) -> Any:
    z = x + y
    return z


print(add(1, 2))
print(add('1', '2'))
print(add('1', 2))  # 无法运行

尽量少用,无法实现类型注解的效果

Optional 可选类型注解

在建立某些对象时,我们可能会出于一定的考虑,将对象先设置为None,部分检查器可能会因为注解与实际类型其不符合,从而给出警告,此时就需要我们的可选类型注解:

python
from typing import Optional

对象名: Optional[可选的一个类名]

使用示例:

python
from typing import Optional


def hello(name: Optional[str] = None) -> str:
    if name:
        return f"早上好! {name}"
    else:
        return f"早上好!"


print(hello('正心'))
print(hello())

Union 可选类型注解

Union 与 Optional 的区别在于,它可以使用多个类型的注解列表(注意:必须是两个及以上的注解),基本格式如下:

python
from typing import Union

对象名: Union[可选类名1, 可选类名2...]

使用示例:

python
from typing import Union


def calculate_list(arr: list) -> int:
    return sum(arr)


def calculate_tuple(arr: tuple) -> int:
    return sum(arr)


def calculate_1(arr: Union[list, tuple]) -> int:
    return sum(arr)


print(calculate_list([1, 2, 3, 4, 5]))
print(calculate_tuple((1, 2, 3, 4, 5)))
print(calculate_1([1, 2, 3, 4, 5]))
print(calculate_1((1, 2, 3, 4, 5)))

使用 | 进行可选注解(Python3.10+)

还有一种新版本的书写方法(Python3.10+):

python
def calculate_2(arr: list | tuple) -> int:
    return sum(arr)


print(calculate_2([1, 2, 3, 4, 5]))

Literal 限制值注解

docs.python.org Literal

Literal 是 Python 中 typing 模块提供的一种注解,用于指定一个变量的值应该是特定字面值之一。它的作用是在类型提示中限制一个变量的取值范围。Literal 的常见用法是在定义函数参数或类属性时,用来明确表示该参数或属性应该是特定的几个值之一,特定值的类型为任何不可变的类型。

python
from typing import Literal


def colorize(is_color: Literal['red', 'green', 'blue']) -> str:
    return is_color


color_type: str = colorize("red")

需要注意的是,Literal 是在 Python 3.8 及以后版本的 typing 模块中引入的,如果你使用的是更早版本的 Python,可能需要考虑其他的方式来达到类似的效果。

cast 伪强制转换

python
typing.cast(typ, val)

typing.cast 的作用是将一个值强制转换为指定的类型,同时告诉类型检查器(例如 Mypy)不要对这次转换进行检查。这在一些特定的情况下很有用,例如当类型检查器无法推断正确的类型时,或者我们确切地知道某个值的类型但类型检查器无法确定,但是实际上它只是骗一下检查器,并不会真的进行转换。

举个例子:

python
from typing import cast


def example_function() -> str:
    flag = None
    flag = cast(str, flag)
    return flag


print(example_function())  # None

下面是一些使用场景:

  1. 类型推断不准确的情况:

    python
    from typing import List, cast
    
    def process_data(data: List[str]) -> None:
        # 在某些情况下,类型检查器无法正确推断 data 的类型
        # 使用 cast 进行强制类型转换,告诉类型检查器 data 确实是 List[str]
        processed_data = cast(List[str], data)
        # 这样可以避免类型检查器的警告
        for item in processed_data:
            print(item)
  2. 处理动态属性或方法:

    python
    from typing import cast
    
    class CustomObject:
        def __getattr__(self, name: str) -> int:
            # 在这里,类型检查器无法确定 getattr 返回的是什么类型
            result = cast(int, getattr(self, name))
            return result + 10
  3. 处理 JSON 数据:

    python
    from typing import Any, Dict, cast
    
    def process_json(data: Dict[str, Any]) -> None:
        # 假设我们知道 "count" 键对应的值是整数,但类型检查器无法确定
        count_value = cast(int, data.get("count", 0))
        print(f"Count: {count_value}")

Final 常量注解

python
Fianl_value_name: Final[type]

示例如下

python
from typing import Final

PI: Final[float] = 3.14

但是,并不会阻止我们修改,改了也能够正常运行

TypedDict 字典类型

TypedDict 是 Python 3.8 中引入的一项特性,属于 PEP 589(TypeHints: Type Hints Customization)的一部分。它提供了一种指定字典中键和值类型的方式,使得能够更精确地进行类型注解,建议使用时针对于静态的字典内容类型。

以下是一个基本的 TypedDict 使用示例:

python
from typing import TypedDict


class Person(TypedDict):
    name: str
    age: int
    email: str

在这个例子中,Person 是一个 TypedDict,指定了三个键:nameageemail,以及它们对应的值类型。这允许你创建一个具有这些键的字典并进行类型检查。

如果尝试为键分配错误类型的值,类型检查器(如 MyPy)将捕获错误并提供反馈。

需要注意的是,TypedDict 主要用于静态类型检查,它不引入运行时检查。它在与工具(如 MyPy)一起使用时非常有用,这些工具可以分析代码并提供与类型相关的反馈。

Required 与 NotRequired(Python3.11+)

peps.python.org/pep-0655/

类型typing.Required限定符用于指示 TypedDict 定义中声明的变量是必需的键(同时也是默认情况下的设置),typing.NotRequired 类型限定符用于指示 TypedDict 定义中声明的变量是非必须的键,下面给出示例:

python
from typing import TypedDict, Required, NotRequired


class Person(TypedDict):
    name: Required[str]
    age: Required[int | None]
    email: NotRequired[str]


my_dict: Person = {'name': "233", 'age': None}

注意:在不是 TypedDict 项目的任何位置使用Required[]或都是错误的。NotRequired[]类型检查器必须强制执行此限制。

Unpack 解包类型

Unpack 的类型主要用于解包参数,特别是在泛型类和函数中,以提供更灵活的类型提示。我们直接以传入位置关键字给函数来展示它的强大之处:

python
from typing import TypedDict, Unpack


class Person(TypedDict):
    name: str
    age: int
    email: str


def occupy_function(**kwargs: Unpack[Person]) -> None:
    print(kwargs)


occupy_function(name="233", age=18, email="xxx@qq.com")

在编辑时就会得到提示

TypeVar 泛型注解

TypeVar 是 Python 中的一个泛型类型的工具,它通常与泛型函数和类一起使用,以增强代码的类型提示和类型检查。TypeVar 允许定义一个类型变量,该变量可以在多个地方使用,并且在使用时可以根据需要绑定到不同的具体类型。

简单泛型注解

python
from typing import TypeVar, List

T = TypeVar('T')


def repeat_item(item: T, n: int) -> List[T]:
    return [item] * n

TAny 类型,接收任意类型参数

协变泛型注解

所谓协变泛型实质上是一种划定了上界的泛型,TypeVar 中的 bound 参数用于指定类型变量的上界(bound),表示类型变量可以是哪些类型的子类型。上界提供了对类型变量进行限制的机制,使得在使用该类型变量时,其实际类型必须符合指定的上界条件。

python
from typing import TypeVar

T = TypeVar('T', bound=SomeType)
  • T:类型变量的名称。
  • bound=SomeType:指定类型变量 T 的上界为 SomeTypeSomeType 可以是一个具体的类型,也可以是一个泛型类型。
  • 限制类型范围: bound 参数允许你限制类型变量的范围,确保它只能是指定类型或指定类型的子类型。
  • 提供类型信息:指定上界后,类型系统可以更精确地推断类型变量的实际类型,提高类型检查的准确性。
  • 支持多重上界:可以指定多个上界,形成联合类型,表示类型变量必须是这些上界中任意一个的子类型。
python
from typing import TypeVar, Union

# TypeVar with a single bound
T = TypeVar('T', bound=int)


def double_value(value: T) -> T:
    return value * 2


# TypeVar with multiple bounds
U = TypeVar('U', bound=Union[int, float])


def add_values(a: U, b: U) -> U:
    return a + b

多类型泛型注解

python
from typing import TypeVar, Union

T = TypeVar('T', int, float)


def display_content(content: T) -> None:
    print(content)


# 使用
display_content(42)  # 合法
display_content(3.14)  # 合法
display_content("Hello")  # 不合法,因为 str 不在允许的类型范围内
display_content(True)  # 不合法,因为 bool 不在允许的类型范围内

在上面的例子中,T 是一个多类型变量(使用此语法必须是两个以上的类型),可以是整数、浮点数。

简单案例

python
from typing import TypeVar


def add1(number1: int, number2: int) -> int:
    number3: int = number1 + number2
    return number3


def add2(number1: float, number2: float) -> float:
    number3: float = number1 + number2
    return number3


add1(1, 2)
add2(1.2, 2.3)

T = TypeVar('T', int, float)


def add(number1: T, number2: T) -> T:
    number3: T = number1 + number2
    return number3


add(1, 1)
add(1.1, 1.1)
add("1", 1.1)  # 错误,泛型类型必须一致 #

自定义类型添加类型注解

python
class Person:
    def __init__(self, name: str) -> None:
        self.name = name

    def get_name(self) -> str:
        return self.name


class Game:
    def __init__(self, object): # 无提示 #
    def __init__(self, object: Person): # 有提示  #
        self.object = object

    def get_user_name(self) -> str:
        return self.object.get_name()


if __name__ == '__main__':
    person = Person('正心')
    game = Game(person)

    print(game.get_user_name())

创建了两个类,在第二个中可以使用第一个类作为注解。

如果不添加类型注释,则无法获取到方法或者查找到许多部分名称相同的方法,尤其是在调用复杂时。如果为 object 变量添加注解,在编写代码时就会出现只能提示。

循环引入

循环引入类或模块问题与解决方法

将前面的代码拆分为两个文件,如下所示

python
from game import Game


class Person:
    def __init__(self, name: str) -> None:
        self.name = name

    def get_name(self) -> str:
        return self.name
python
from person import Person


class Game:
    def __init__(self, object: Person):
        self.object = object

    def get_user_name(self) -> str:
        return self.object.get_name()

运行之后就会报循环引入的错误,导致爆栈,python 对此将会终止脚本运行

首先先讲一个细节,注解内容是字符串时,注解语法同样也是生效的,原因见后文的 annotations

python
from person import Person


class Game:
    def __init__(self, object: 'Person'):  
        self.object = object

    def get_user_name(self) -> str:
        return self.object.get_name()

明确这点后,在导入时使用以下语句进行处理,并且依次简要介绍其功能:

python
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from person import Person

__future__实现向后兼容语言特性

docs.python.org future

__future__是一个特殊的模块,用于帮助实现向后兼容性和引入新的语言特性。通过在代码中导入__future__模块并使用其中的特定功能,可以在较旧的 Python 版本中启用一些即将成为默认行为的特性。这样可以使原有的代码能在不同版本的 Python 中更加一致地运行。

例如,在 Python 2.x 中想要使用 Python 3.x 的print语法,可以在代码的开头添加如下导入语句:

python
from __future__ import print_function

这将启用 Python 3.x 中的print函数,而不是 Python 2.x 中的print语句。

  • 常见使用
特性描述
print_function启用使用print()函数而不是print语句。
division启用 Python 3.x 中的真正除法(即/操作符执行浮点除法),而不是 Python 2.x 中的整数除法。
absolute_import修改导入语句的语义,确保使用绝对导入,而不是相对导入。
unicode_literals启用字符串文字为 Unicode 字符串而不是字节字符串。
generators修改next()函数的语法以适应 Python 3.x 中生成器的语法。

annotations 改变类型注解行为

在 Python 中,from __future__ import annotations 是一个特殊的语法,用于调整类型注解的处理方式。在默认情况下,类型注解在函数签名中会被当作字符串处理,这导致了一些限制,特别是在涉及到递归类型注解时。annotations 的引入就是为了解决这个问题。

使用 from __future__ import annotations 可以改变类型注解的行为,使得类型注解在函数签名中不再被当作字符串处理(默认处理方式),而是被直接处理为类型。这使得在类型注解中引用同一模块中定义的类名等符号时,不再需要将类型注解放在字符串引号中。

这个改变的目的是为了简化类型注解的书写,特别是在定义复杂的数据结构或递归类型时,使得代码更加清晰和易读。这个特性从 Python 3.7 版本开始引入,但需要使用 from __future__ import annotations 才能启用。

__annotations__属性

__annotations__ 是 Python 中一个特殊的属性,用于访问包含函数或类成员中类型注解的字典。当在函数或类中使用类型注解时,解释器会将这些注解存储在 __annotations__ 字典中。示例:

python
def example(a: int, b: str) -> float:
    pass


print(example.__annotations__)
# 输出:{'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}

在这个例子中,__annotations__ 字典包含了参数 ab 以及返回值的类型信息。这对于在运行时访问这些信息或者通过代码分析工具来检查和分析代码非常有用。然而,如果在使用 from __future__ import annotations 语句的情况下,__annotations__ 的行为会有所不同。在这种情况下,__annotations__ 不再包含字符串形式的类型注解,而是直接包含解释后的类型信息。这使得在代码中不再需要使用字符串引号来表示类型,而直接使用类型符号。

python
from __future__ import annotations


def example(a: int, b: str) -> float:
    pass


print(example.__annotations__)
# 输出:{'a': 'int', 'b': 'str', 'return': 'float'}

通过引入 from __future__ import annotations__annotations__ 中存储的是直接的类型对象,而不是字符串。这在某些情况下可以提高代码的可读性,尤其是在处理递归类型注解等情况。

类的__annotations__

注意:直接访问一个类,无法获取到__annotations__ ,将会获得一个空字典:在类级别定义的类型注解通常用于提供文档信息,而它们并不像函数级别的那样在运行时被自动存储在 __annotations__ 属性中。在类中使用类型注解时,这些注解通常是用于文档生成工具(如 Sphinx)的目的,而不会被 Python 解释器直接存储。

可以使用 __annotations__ 属性的方式是在类的 __init__ 或其他方法中引入 __annotations__ 属性,以确保类型信息被存储在类的 __annotations__ 属性中。例如:

python
class MyClass:
    __annotations__ = {'attribute': int}


print(MyClass.__annotations__)  # 输出:{'attribute': <class 'int'>}

TYPE_CHECKING 解决循环引入问题

TYPE_CHECKINGtyping 模块中的一个特殊变量,用于解决在类型注解中避免循环导入问题的情况。

在 Python 中,当两个模块相互导入时,可能会导致循环导入的问题,其中一个模块引用了另一个模块,而另一个模块又引用了第一个模块,从而形成了循环。这可能会导致在运行时出现 ImportError。

为了解决这个问题,typing 模块中引入了一个特殊的变量 TYPE_CHECKING。这个变量在运行时始终为 False ,但在类型检查工具(如 mypy)运行时为 True。因此,可以使用 TYPE_CHECKING 变量在类型注解中创建条件导入,从而避免循环导入问题。

示例:

python
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from person import Person


class Game:
    def __init__(self, object: 'Person'):
        self.object = object

    def get_user_name(self) -> str:
        return self.object.get_name()

在这个例子中,TYPE_CHECKING 被用于创建条件导入。当运行时 TYPE_CHECKINGFalse ,因此实际的导入语句不会执行,而在类型检查时,TYPE_CHECKINGTrue ,导入语句会被执行,从而避免了循环导入问题。这种技术特别在涉及复杂类之间的相互引用和类型注解的情况下有用。

类相关

阻止继承装饰器 final

使用 @final 装饰器来阻止继承注解

python
class Animal:
    def __init__(self, name) -> None:
        self.name = name

    @final  # 注解之后,该函数子类不能重写 #
    def greeting(self) -> str:
        return "早上好,你吃了没?"

与上面一致,仅仅只是声明,并不会妨碍运行

完整代码如下:

python
from typing import final


class Animal:
    def __init__(self, name) -> None:
        self.name = name

    @final  # 注解之后,该函数子类不能重写 #
    def greeting(self) -> str:
        return "早上好,你吃了没?"


class Tiger(Animal):
    def __init__(self, name) -> None:
        super().__init__(name)

    # 子类重写,通不过类型检测 #
    def greeting(self) -> None:  
        return "早上好。"

重载装饰器 overload

在 Python 中,方法签名覆盖(Method Signature Overriding)是一种技术,它允许子类中的方法具有与父类中的方法相同的名称,但具有不同的参数类型或个数。这可以通过使用 @overload 装饰器来实现。

需要注意的是,@overload 装饰器本身并不实际执行任何代码,它只是用于静态类型检查和文档生成。真正的实现位于装饰器下方的实际方法定义中。

python
from typing import overload


class Shape:
    def draw(self):
        print("Drawing a shape")


class Circle(Shape):
    @overload
    def draw(self, color: str) -> None:
        pass

    def draw(self, color: str = "red"):
        print(f"Drawing a {color} circle")


if __name__ == "__main__":
    circle = Circle()
    circle.draw()  # 输出:Drawing a red circle
    circle.draw("blue")  # 输出:Drawing a blue circle

重写装饰器 override

该方法是 Python3.12 新加入的,区分两者:

  • overload 装饰器用于标记方法的多个版本,但真正的重载需要在实现中手动处理。
  • override 注解用于确保子类中的方法正确地覆盖了父类中的方法。
python
from typing import override


class BaseClass:
    def occupy_function(self) -> None:
        pass


class SubClass(BaseClass):
    @override
    def occupy_function(self) -> None:
        print("Hello World")


if __name__ == "__main__":
    object: SubClass = SubClass()
    object.occupy_function()  # 输出 Hello World

链式调用 Self 注解

What's New In Python 3.11

什么是链式调用

在 Python 中,链式调用是指通过在方法调用的返回值上连续调用其他方法,从而形成一条方法调用的链。这种风格通常用于构建一系列相关的操作,使代码更加紧凑和可读。要实现链式调用,每个方法都需要返回一个对象,这个对象上有下一个方法可以被调用。

考虑以下示例,演示链式调用和返回方法本身的概念,并且进行类型验证:

python
class Calculator:
    def __init__(self, value: float = 0) -> None:
        self.value = value

    def add(self, x: float) -> 'Calculator':
        self.value += x
        return self  # 返回方法本身,以支持链式调用

    def subtract(self, x: float) -> 'Calculator':
        self.value -= x
        return self  # 返回方法本身,以支持链式调用

    def multiply(self, x: float) -> 'Calculator':
        self.value *= x
        return self  # 返回方法本身,以支持链式调用

    def divide(self, x: float) -> 'Calculator':
        if x != 0:
            self.value /= x
        else:
            print("Error: Division by zero.")
        return self  # 返回方法本身,以支持链式调用


if __name__ == "__main__":
    # 使用链式调用
    result = Calculator(10).add(5).subtract(3).multiply(2).divide(4).value
    print(result)  # 输出:6.0

    # 打印类型和检查实例
    print(type(Calculator(10).add(5)))  # 输出:<class '__main__.Calculator'>
    print(isinstance(Calculator(10).add(5), Calculator))  # 输出:True

在上述示例中,Calculator 类有四个方法:addsubtractmultiplydivide。每个方法在完成相应的操作后都返回 self ,这样就可以在方法调用之后继续调用其他方法。

通过创建 Calculator 对象并进行链式调用,可以一步步地执行一系列数学操作,并且最终通过访问 value 属性获取结果。这种模式使代码看起来更加流畅和易于理解。

要注意的是,返回方法本身并进行链式调用的做法是一种设计选择,并不是所有类都需要这样做。在某些情况下,这可能使代码更简洁,但在其他情况下,可能会降低代码的可读性。

使用 Self 注解方式示例

python
from typing import Self


class Calculator:
    def __init__(self, value: float = 0) -> None:
        self.value = value

    def add(self, x: float) -> Self:
        self.value += x
        return self

    def subtract(self, x: float) -> Self:
        self.value -= x
        return self

    def multiply(self, x: float) -> Self:
        self.value *= x
        return self

    def divide(self, x: float) -> Self:
        if x != 0:
            self.value /= x
        else:
            print("Error: Division by zero.")
        return self

ClassVar 类属性注解

在 Python 类型注解中,ClassVar 是一个用于表示类属性的注解。类属性是属于类而不是实例的属性。使用 ClassVar 注解可以更明确地指定一个变量是类属性,并且该注解只能接收一个类型。

下面是一个使用 ClassVar 注解的示例:

python
from typing import ClassVar


class MyClass:
    class_attribute: ClassVar[int] = 42

    def __init__(self, instance_attribute: str) -> None:
        self.instance_attribute: str = instance_attribute


if __name__ == "__main__":
    # 访问类属性
    print(MyClass.class_attribute)  # 输出:42

在上述例子中,class_attribute 被注解为 ClassVar[int],表示这是一个整数类型的类属性。这样的注解提供了更明确的类型信息,有助于类型检查工具识别和捕获潜在的类型错误。

需要注意的是,在 Python 3.10 及以后的版本中,ClassVar 注解是可选的,因为 Python 可以根据上下文自动推断出类属性。在较早的版本中,可能需要使用 ClassVar 注解来提供类型信息。

协变与逆变

参考:【Python-协变与逆变】peps.python.org/pep-0484

在 Python 中,泛型的协变(covariance)和逆变(contravariance)是指在类型系统中,类型参数的子类型关系是如何随着容器类型的变化而变化。

协变(covariance)

  • 当一个类型的子类型的顺序与原始类型的子类型的顺序相同时,我们称之为协变。
  • 在协变中,如果 AB 的子类型,那么 List[A] 就是 List[B] 的子类型。

示例:

python
class Animal:
    pass


class Dog(Animal):
    pass


animals: List[Animal] = [Dog(), Animal()]  # 协变

在这个例子中,List[Dog]List[Animal] 的子类型,因为DogAnimal的子类型,所以 Animal 类型的容器可以安全容纳 Dog 类型的对象。

逆变(Contravariance)

  • 当一个类型的子类型的顺序与原始类型的子类型的顺序相反时,我们称之为逆变。
  • 在逆变中,如果 AB 的子类型,那么 Callable[B] 就是 Callable[A] 的子类型。

示例:

python
from typing import Callable


class Animal:
    pass


class Dog(Animal):
    pass


def handle_animal_with_dog(dog_handler: Callable[[Dog], None], dog: Dog) -> None:
    dog_handler(dog)


def handle_animal(animal: Animal) -> None:
    print("Handling a animal")


# 逆变:将处理狗的函数传递给处理动物的函数
handle_animal_with_dog(handle_animal, Animal())

在这里我们调用了 handle_animal_with_dog(handle_animal, Animal())。在这里,我们将处理 Animal 的函数 handle_animal 传递给了处理狗的函数 handle_animal_with_dog。这正是逆变的概念的体现。尽管 handle_animal_with_dog 函数的参数类型签名是 Callable[[Dog], None],即它期望一个接受 Dog 类型参数的函数,但是我们却将一个接受更抽象类型 Animal 的函数 handle_animal 传递给它。这是因为函数参数的逆变性质,即可以传递一个接受更抽象类型的函数给接受更具体类型的函数参数。

动态函数调用

在 Python 中,将方法名传递给方法通常使用的是函数引用。在 Python 中,函数是第一类对象,这意味着它们可以被当作值传递、赋值给变量,并作为参数传递给其他函数。当你将方法名传递给另一个方法时,你实际上是将函数引用传递给它。

下面是一个简单的示例,说明如何将方法名传递给方法:

python
def greet(name):
    return "Hello, " + name


def farewell(name):
    return "Goodbye, " + name


def perform_greeting(greeting_function, name):
    result = greeting_function(name)
    print(result)


# 将方法名 greet 传递给 perform_greeting 方法
perform_greeting(greet, "Alice")

# 将方法名 farewell 传递给 perform_greeting 方法
perform_greeting(farewell, "Bob")

在这个例子中,perform_greeting 方法接受一个函数引用作为参数,并调用它来执行问候。这种方式允许动态地选择要执行的函数,从而增加程序灵活性。

Generic 抽象基类

Generic 是 Python 中 typing 模块提供的一个抽象基类(Abstract Base Class),用于表示泛型类。在 typing 模块中,Generic 用于定义泛型类,即可以处理多种数据类型的类。通过在类定义中使用 Generic[T],其中 T 是类型变量,可以在实例化时指定具体的类型。

单泛型参数实现

python
from typing import Generic, TypeVar

T = TypeVar('T')


class Box(Generic[T]):
    def __init__(self, content: T) -> None:
        self.content = content

    def get_content(self) -> T:
        return self.content


if __name__ == "__main__":
    int_box: Box[int] = Box(42)
    str_box: Box[str] = Box("Hello, Generics!")

    # 获取并打印内容
    print("Integer Box Content:", int_box.get_content())  # 输出:42
    print("String Box Content:", str_box.get_content())  # 输出:Hello, Generics!

多泛型参数实现

python
from typing import Generic, TypeVar

T = TypeVar('T')
U = TypeVar('U')


class Pair(Generic[T, U]):
    def __init__(self, first: T, second: U) -> None:
        self.first: T = first
        self.second: U = second


if __name__ == "__main__":
    int_str_pair: Pair[int, str] = Pair(42, "Hello, Generics!")

    # 获取并打印内容
    print("First Element:", int_str_pair.first)  # 输出:42
    print("Second Element:", int_str_pair.second)  # 输出:Hello, Generics!

多继承泛型实现

python
from typing import Generic, TypeVar, List

T = TypeVar('T', int, str)
U = TypeVar('U', int, str)


class Container(Generic[T, U]):
    def __init__(self, items: List[T], metadata: U) -> None:
        self.items: List[T] = items
        self.metadata: U = metadata


class Box(Generic[T, U], Container[T, U]):
    def __init__(self, content: T, items: List[T], metadata: U) -> None:
        super().__init__(items, metadata)
        self.content: T = content


if __name__ == "__main__":
    int_str_box: Box[int, str] = Box("233hhh", [1, 2, 3, 4, 5], "Box Metadata")

    # 获取并打印内容
    print("Box Content:", int_str_box.content)  # 输出:233hhh
    print("Container Items:", int_str_box.items)  # 输出:[1, 2, 3, 4, 5]
    print("Container Metadata:", int_str_box.metadata)  # 输出:"Box Metadata"

Protocol 抽象基类

在 Python 中,typing 模块提供了 Protocol 抽象基类,用于定义协议。Protocol 允许你明确地声明一个类应该具有哪些属性、方法或其他特征,从而实现协议。下面我们来实现一个协议:

python
from typing import Protocol, ClassVar


class AnimalProtocol(Protocol):
    sound: ClassVar[str]

    def make_sound(self) -> None:
        ...

    def move(self, distance: float) -> None:
        ...

    def eat(self, food: str) -> None:
        ...


class Cat:
    sound = "Meow"

    def __init__(self, name: str) -> None:
        self.name = name

    def make_sound(self) -> None:
        print(f"{self.name} says {self.sound}")

    def move(self, distance: float) -> None:
        print(f"{self.name} moves {distance} meters gracefully")

    def eat(self, food: str) -> None:
        print(f"{self.name} enjoys eating {food}")


class Dog:
    sound = "Woof"

    def __init__(self, name: str, breed: str) -> None:
        self.name = name
        self.breed = breed

    def make_sound(self) -> None:
        print(f"{self.name} barks {self.sound}")

    def move(self, distance: float) -> None:
        print(f"{self.name} runs {distance} meters energetically")

    def eat(self, food: str) -> None:
        print(f"{self.name} devours {food}")


if __name__ == "__main__":
    cat = Cat(name="Whiskers")
    dog = Dog(name="Buddy", breed="Golden Retriever")

    # Cat-specific property
    print(f"{cat.name} is a cat")

    # Dog-specific property
    print(f"{dog.name} is a {dog.breed} dog")

    # Common behavior
    cat.make_sound()
    cat.move(2.5)
    cat.eat("fish")

    dog.make_sound()
    dog.move(5.0)
    dog.eat("bones")

在 Python 中,... 是一个特殊的语法,表示一个占位符,通常用于表示某个代码块未实现或者不需要实现。在协议中,... 的作用是指示方法的声明,而不需要提供具体的实现。

协议的目的是定义一组规范,描述类应该具有的方法、属性或其他特征,而不是要求在协议中提供具体的实现。因此,使用 ... 作为占位符是合理的,因为协议只是定义了接口,而不关心具体的实现逻辑

Generator 生成器注解

生成器注解分为三个部分,并且每个部分都必须完备:

python
 Generator[生成值类型, 接收参数类型, 返回参数类型]
  • 直接上使用示例

通常来讲生成器是没有返回参数的,但是某些情况下,我们可能会返回一些内容作为异常时的返回内容

python
 from typing import Generator


def generator_of_numbers(generate_range: int) -> Generator[int, None, int]:
    for i in range(generate_range):
        yield i
    return generate_range


if __name__ == "__main__":
    gen = generator_of_numbers(5)

    try:
        while True:
            print(next(gen), end='  ')
    except StopIteration as e:
        print("\nThe range of generator:", e.value)

Iterable 迭代器注解

迭代器注解主要用在迭代器对象上,用于标记__iter__方法:

python
 from typing import Iterator


class MyIterator:
    def __init__(self, max_num: int):
        self.max_num: int = max_num
        self.current_num: int = 0

    def __iter__(self) -> Iterator[int]:
        return self

    def __next__(self) -> int:
        if self.current_num < self.max_num:
            self.current_num += 1
            return self.current_num
        else:
            raise StopIteration


if __name__ == "__main__":
    my_iterator = MyIterator(5)
    for num in my_iterator:
        print(num)

参考

https://juejin.cn/post/7332227724801736756

Python-typing 官方文档:https://docs.python.org/3/library/typing.html

Python-官方文档 Type Hints:【PEP 484 – Type Hints】https://peps.python.org/pep-0484/

Python-官方文档 Self Type:【PEP 673 – Self Type】https://peps.python.org/pep-0673/#abstract

https://juejin.cn/user/2746994901126669/posts