Python类型注释


Python类型注释

Python类型注释

本文编写于2023年11月24日,Python的Type Hint因为正在经历快速的开发迭代,因此本文针对最新的Type Hint进行讲解。

1. 前言

Python是一种动态类型语言,这意味着我们不需要在声明变量时指定其类型,Python解释器会在运行时自动确定变量的类型。

1.1 绑定(Binding)

在Python中,Binding是一个重要的概念。

在Python中,变量名和对象之间的关联被称为Binding。Python中的所有东西都是对象。这包括数字(如整数和浮点数)、字符串、列表、字典、函数、类,甚至模块等等。这些都是对象,它们都有一个类型,它们都可以有属性和方法。

而当我们在Python中声明一个变量的时候,Python干的事情就是:

  • 首先在内存中创建一个对应类型的对象
  • 然后将内存中对象绑定(bind)到这个变量名上

未来我们就可以通过变量名来访问这个对象的属性和方法。例如:

x = 56

Python在内存中为我们创建了一个 int 对象,值为56,然后将其与变量名 x 绑定。接下来我们就可以使用int这个类的方法了,例如:

print(x.to_bytes()) # Output: b'8'

8就是ASCII码中56的值

PS: 我们这里不讨论Python会提前创建一些对象以加速运行

Binding为Python带来了极大的灵活性,允许我们编写更通用的代码。

1.2 鸭子类型(Duck Type)

“鸭子类型”(Duck Typing)是一种编程概念,它在动态类型的语言中,如Python,非常常见。这个概念的名字来源于一个古老的表达:

  • 如果它走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子

在编程语境中,鸭子类型意味着我们并不关心对象是什么类型,我们只关心对象能做什么。换句话说,一个对象的行为(即它的方法和属性)比它的实际类或类型更重要。

例如:

class Duck:
    def quack(self):
        return "Quack!"

class Dog:
    def quack(self):
        return "I pretend to be a duck. Quack!"

def make_it_quack(animal):
    print(animal.quack())

duck = Duck()
dog = Dog()

make_it_quack(duck)  # Output: Quack!
make_it_quack(dog)   # Output: I pretend to be a duck. Quack!

在鸭子类型的观念下,我们并不关心传递给 make_it_quack 的对象是什么类型,只要传入的对象有一个 .quack() 方法就可以。这意味着我们可以传递任何实现了 .quack() 方法的对象给这个函数,而不仅仅是某个特定的 Duck 类的实例。从而使得make_it_quack函数更加通用,避免了C++中需要对同一个函数进行重载或者使用模板。

1.3 问题

然而,随着Python在大规模和复杂的项目中的广泛应用,Binding带来的一些缺点开始显现出来:

  1. 可读性和可维护性:在没有类型信息的情况下,阅读和理解代码可能会变得困难,特别是对于大型的代码库。重重跳转之后就不知道某个变量到底是什么类型了
  2. 错误检测:动态类型语言在运行时才检查类型错误,有可能某个地方将变量名绑定到了另外一个类的实例上,如果恰好也有相同的方法(即鸭子类型),这可能导致一些难以发现的bug

1.4 解决之道: 类型注解(Type Hint)

Python动态类型带来的最主要的问题就是我们不知道某个变量绑定的对象的类型,因此如果只要我们能够知道某个类的类型,就可以解决这些问题。

因此Python中引入了类型注释Type Hint)的功能,使得开发者可以选择性地为变量、函数参数和返回值添加类型信息:

  • 一方面,这些类型信息告知了其他审阅你代码的人这个变量应该是什么类型,方便了维护和理解
  • 另外一方面,一些工具可以在不运行代码的前提下检查这些类型,从而减少实际调试中的难度
  • 第三,类型注解也可以被IDE和其他工具用来提供更好的代码补全和错误检查功能,在推断出变量的类型后,IDE可以更快、更轻松的给出补全信息

mypy是一个Python类型静态检查工具

所以Type Hint对Python来说还是非常有用的,在精进Python的路上还是有必要要学习一下的。

2. Type Hint发展历史

PEP上的讨论,我们能够得知Type Hint的发展历史。要了解Python Type Hint的发展历史,就得先了解什么是PEP

这部分的内容如果不感兴趣的话,可以直接跳转到后面介绍Type Hint语法的部分

2.1 PEP (Python Enhancement Proposal)

PEPPython Enhancement Proposal的缩写,意为Python增强提案。它是Python社区提出和讨论Python语言改进和新特性的一种正式机制。

每个PEP都是一个设计文档,可能描述了一个新的特性,或者解释了某个问题,或者描述了一些新的或修订的过程或环境。PEP应该提供足够的信息来帮助社区理解提案的问题和解决方案。

PEP分为三种类型:

  1. Standards Track PEPs:这些PEP描述了新的特性或实现,或者描述了一个问题并提出了解决方案。在未来的Python版本中可能会引入这些新的特性。
  2. Informational PEPs:这些PEP描述了Python设计问题,或者为Python社区提供了一些通用的信息或指导。但是,这些PEP通常只是讨论,只有足够的有用之后才可能被正式纳入到Python未来的改进中
  3. Process PEPs:这些PEP描述了Python社区的过程,或者提出了一个过程改进。它们类似于Standards Track PEPs,但是它们通常不改变Python的实现,只改变Python社区的流程,即对Python社区改进提出了意见

一个PEP的声明周期通常包括草案提出、讨论、修改、投票,以及最终的接受或拒绝。所有的PEP都可以在Python的官方网站上找到:https://peps.python.org/

Python PEP社区

2.2 PEP中Type Hint的发展历史

简要的历史如下:

  • PEP 3107 - Function Annotations:在Python 3.0中引入,这个PEP提供了函数注解的基础语法,但并没有规定该如何解析注释,只是提供了一个建议。
  • PEP 484 - Type Hints:在Python 3.5中引入,这个PEP正式引入了类型提示系统。它定义了基本的类型提示语法,以及一些标准的类型(如ListDict等),并引入了typing模块。
  • PEP 526 - Syntax for Variable Annotations:在Python 3.6中引入,这个PEP扩展了类型提示系统,允许在变量声明中使用注解来指定变量的预期类型。
  • PEP 563 - Postponed Evaluation of Type Annotations:在Python 3.7中引入,这个PEP改变了类型注解的解析方式,使得它们在运行时不会立即被评估。这解决了一些关于循环引用和前向引用的问题。
  • PEP 585 - Type Hinting Generics In Standard Collections:在Python 3.9中引入,这个PEP允许直接在标准集合类型(如listdict等)上使用泛型类型提示,而不需要使用typing模块中的对应类型(如ListDict等)。
  • PEP 646 - Variadic Generics:在Python 3.10中引入,这个PEP引入了可变泛型,允许在类型提示中更准确地表示可变数量的类型。
  • PEP 612 - Parameter Specification in Type Hints:在Python 3.10中引入,这个PEP提供了一种新的方式来表示复杂的参数类型,特别是那些使用了*args**kwargs的函数。
  • PEP 604 - Allow writing union types as X | Y:在Python 3.10中引入,这个PEP允许使用X | Y的语法来表示联合类型,而不需要使用typing.Union

PEP 3107: Function Annotations

PEP 3107提出于2006年12月2日,最终在Python 3.10中引入。最初的目的是希望给Python的函数中添加注释,从而帮助读者理解函数的每个类型,从而避免鸭子类型带来的烦恼。

在PEP 3107之前,Python的函数声明看起来大概是这样的:

def foo(bar, baz):
    ...

我们完全不知道barbaz到底是什么类型的参数,从而导致难以理解foo函数的作用。

PEP 3107提出了一个建议,就是为Python引入新的语法,允许你在函数声明中添加注解,例如:

def foo(bar: 'a string', baz: int) -> float:
    ...

在这个例子中,'a string'int是参数注解,float是返回注解。PEP 3107中提出,注解可以是任何Python表达式,它们在函数定义时被评估,并在函数的__annotations__属性中存储,以帮助静态类型检查工具进行检查。

需要注意的是,PEP 3107本身并没有定义该如何解析注解,只是提出了一个建议。注解的解析由第三方库和工具决定。

PEP 484: Type Hints

尽管PEP 3107中只是提出了该如何对函数原型进行注释,但是其中已经出现了对变量的类型注释。因此经过了几个版本的发展后,终于Type Hint在PEP 484中被进一步讨论,并被引入了Python 3.5版本中。

值得一提的是,PEP 484Python之父Guido van Rossum亲自提出得,可见Type Hint之重要性

PEP 484正式的为Python引入了类型注释(Type Hints)系统,这是一种标准化的方式来表明函数参数和返回值的预期类型,允许开发者在代码中明确地指定变量、函数参数和返回值的预期类型。

例如,下面的函数使用类型提示来表明参数 xy 应该是整数,返回值应该是浮点数:

def divide(x: int, y: int) -> float:
    return x / y

除了正式规定Type Hint的语法之外,PEP 484还定义了一个 typing 模块,提供了一些用于表示复杂类型的类和函数,例如 ListDictTupleOptional 等。例如:

from typing import List, Tuple

def process_data(data: List[Tuple[int, int]]) -> None:
    for x, y in data:
        ...

PEP 526: Syntax for Variable Annotations

PEP 3107PEP 484中,类型注解都仅针对函数,而没有为变量提供类型注解。因此PEP 526扩展了Type Hint,允许在变量声明中使用注解来指定变量的预期类型。

例如:

# 注释Python内置类型的变量
x: int = 1

# 注释自定义类型的变量
class MyClass:
    pass

y: MyClass = MyClass()

# 注释变量的类型
z: int

# 为类属性注释类型
class MyClass:
    x: int
    y: str = "hello"

# 注释一个所有元素为int的list
numbers: List[int] = []

PEP 563: Postponed Evaluation of Type Annotations

PEP 563之前,类型注解在定义时就会被解析。这意味着如果你在类型注解中引用了一个还没有定义的类,Python会抛出一个NameError

例如下面定义链表节点的代码:

class Node:
    def add_child(self, node: "Node") -> None:  # Python在解析Type Hint时,解析到这里,因为Node还没有被完全定义,所以如果不加引号会报一个NameError的错误
        ...

因此,PEP 563中提出:为了解决这个问题,可以将类型注解的解析推迟到了运行时。这意味着你可以在类型注解中直接引用还没有定义的类,而不需要使用字符串。以上面的例子为例,使用PEP 563,我们可以这样写:

from __future__ import annotations

class Node:
    def add_child(self, node: Node) -> None:  # 在这里,Node可以直接被引用
        ...

需要注意的是,虽然PEP 563在Python 3.7中被引入,但是你需要通过from __future__ import annotations来启用它。在Python 3.10及以后的版本中,PEP 563成为了默认行为,不再需要导入__future__模块。

在Python中,__future__模块是一个特殊的模块,它允许你在当前的Python版本中使用一些在未来Python版本才会被引入的新特性。新特性通常是在PEP中讨论之后决定要引入的新特性。

这个模块的名字中的”future”并不是指真正的未来,而是指未来的Python版本。当Python的核心开发者决定引入一些可能会改变Python语法或者语义的新特性时,他们通常会首先将这些新特性作为__future__模块中的可选特性引入,以便开发者在未来的Python版本正式引入这些新特性之前就可以开始使用它们。

这样做的好处是,开发者可以在新特性正式引入并成为默认行为之前,就有足够的时间来适应这些新特性。同时,这也给了开发者一个机会,让他们可以在新特性正式引入之前,就对它们进行测试,以确保它们不会引入新的bug。

PEP 585: Type Hinting Generics In Standard Collections

PEP 585之前,对listdict等复合类型进行注释需要从Typing模块中导入ListDict等对象。而PEP 585对此进行了改进,使得标准集合类型如listdictsettuple可以直接用于类型提示,而不再需要从typing模块导入对应的大写形式,如ListDictSetTuple

在PEP 585之前,如果你想给一个列表变量添加类型注解,你需要这样写:

from typing import List

def greet_all(names: List[str]) -> None:
    for name in names:
        print(f"Hello, {name}!")

但是在PEP 585之后,你可以直接使用内置的list类型作为类型注解:

def greet_all(names: list[str]) -> None:
    for name in names:
        print(f"Hello, {name}!")

这样的改变使得类型注解的语法更加简洁和直观。此外,它还允许我们在类型注解中使用更多的内置类型,例如typecollections.abc模块中的类型。

需要注意的是,虽然PEP 585在Python 3.9中被引入,但是直到Python 3.10,才将from __future__ import annotations的行为作为默认行为。这意味着在Python 3.9中,如果你想使用PEP 585的特性,你需要在你的代码中添加from __future__ import annotations这一行。

PEP 646: Variadic Generics

PEP 646中提出可以在Python类型注释系统中引入变长泛型的提议。变长泛型是一种允许泛型接受任意数量类型参数的特性。

在一些情况下,我们可能需要表示一个元组的长度是未知的,或者一个函数可以接受任意数量的参数,这些参数的类型都是已知的。在这些情况下,变长泛型就非常有用。

例如,假设我们有一个函数,这个函数接受一个字符串和一个元组,元组中的每个元素都是整数。我们可以使用Tuple[int, ...]来表示这个元组的类型。但是,如果我们想要表示这个元组中的每个元素都可以是不同的类型,那么我们就需要使用变长泛型。

在PEP 646中,我们可以使用Tuple[*Ts]来表示一个元组,其中Ts使用PEP 646中提出的语法进行定义,用以表示多种类型。例如,定义Ts(int, str, float),则Tuple[*Ts]可以表示一个元素可以是intstrfloat等任何类型的元组

PEP 612: Parameter Specification in Type Hints

在Python中,任何东西都是对象,函数也不例外。函数是一个function object,对应于function class,具有__call__方法。例如:

def add(x, y):
  return x + y

add(1, 2)

当定义函数的时候,即def add(x, y)时,Python会创建一个function object,然后把这个function object绑定到add这个变量上。我们调用add(1,2)时候实际上就是调用add.__call(1,2)__这个方法。

正是因为Python的函数也是一个对象这个特性,在Python中函数也可以作为参数,传入到别的函数中,或者其他函数的返回值。这种以函数为参数或返回值的函数称为高阶函数。例如:

# 定义一个函数,接收两个参数:一个函数f和一个值x
def apply_function(f, x):
    return f(x)

# 定义一个简单的函数,计算一个数的平方
def square(x):
    return x**2

# 我们可以将square这个函数对象作为参数传递给apply_function函数
result = apply_function(square, 5)
print(result)  # 输出25,因为5的平方是25

# 函数也可以作为其他函数的返回值
def get_operation(name):
    if name == 'square':
        def square(x):
            return x**2
        return square  # 返回一个函数对象
    elif name == 'cube':
        def cube(x):
            return x**3
        return cube  # 返回一个函数对象

# 获取一个函数
operation = get_operation('square')
print(operation(5))  # 输出25,因为5的平方是25
print(get_operation("cube")(5)) # 输出125,因为返回的函数对象是绑定到cube这个函数上的,所以调用cube(5)得到的值就是125

但是在PEP 612之前,Python的类型提示系统并不能很好地处理函数作为参数和返回值的情况。通过引入ParamSpecConcatenate,我们可以更准确地描述高阶函数的类型。

PEP 604: Allow writing union types as X | Y

PEP 604引入了一种新的方式来表示一个变量可能是多种类型,即使用X | Y的语法。这是Python类型注解中的一个改进,使得表示类型变得更加简洁和直观。

PEP 604之前,如果你想表示一个变量可以是多种类型中的一种,你需要使用typing.Union。例如,如果一个变量可以是整数或字符串,你需要这样表示:

from typing import Union

def func(x: Union[int, str]):
    pass

PEP 604引入了|运算符,使得你可以更简洁地表示同样的意思:

def func(x: int | str):
    pass

介绍完了Type Hint的基础发展历史,下面我们正是介绍一下Python Type Hint的语法

3. Type Hint基础

3.1 变量注释——基础

# 为变量注解类型的基本方式
age: int = 1

# 不需要初始化一个变量来注解它
a: int

# 在条件分支中这样做可以帮助代码检查工具和补全工具
child: bool
if age < 18:
    child = True
else:
    child = False

3.2 变量注释——进阶

# 对于大多数类型,只需要在注解中使用类型的名称
# 注意,IDE通常可以从变量的值推断出变量的类型,所以从技术上讲,这些注解是多余的
x: int = 1
x: float = 1.0
x: bool = True
x: str = "test"
x: bytes = b"test"

# 对于Python 3.9+,列表和集合项的类型放在方括号中
x: list[int] = [1]
x: set[int] = {6, 7}

# 对于字典,需要指定键和值的类型
x: dict[str, float] = {"field": 2.0}  # Python 3.9+

# 对于固定大小的元组,需要指定所有元素的类型
x: tuple[int, str, float] = (3, "yes", 7.5)  # Python 3.9+

# 对于可变大小的元组,指定元素的默认类型然后使用省略号
x: tuple[int, ...] = (1, 2, 3)  # Python 3.9+

# 在Python 3.8及更早版本中,容器类型的名称是大写的,并且是从'typing'模块导入的
from typing import List, Set, Dict, Tuple
x: List[int] = [1]
x: Set[int] = {6, 7}
x: Dict[str, float] = {"field": 2.0}
x: Tuple[int, str, float] = (3, "yes", 7.5)
x: Tuple[int, ...] = (1, 2, 3)

from typing import Union, Optional

# 在Python 3.10+上,当变量可能是几种类型之一时,使用 | 运算符
x: list[int | str] = [3, 5, "test", "fun"]  # Python 3.10+
# 在早期版本中,使用Union
x: list[Union[int, str]] = [3, 5, "test", "fun"]

# 对于可能为None的变量,使用Optional[X],Optional[X]与X | None 或 Union[X, None]相同
x: Optional[str] = "something" if some_condition() else None
if x is not None:
    # 静态类型检查工具和补全工具会自动推理出来在这里x不会是None,因为有if语句
    print(x.upper())
# 如果某个变量可能是None,但是通过前面代码的逻辑你知道在这里这个永远不会是None,则使用断言通知代码补全工具和静态检查工具
assert x is not None
print(x.upper())

3.3 函数注释

from typing import Any, Callable, Iterator, Union, Optional

# 为一个函数进行注释
def stringify(num: int) -> str:
    return str(num)

# 多个参数的函数则依次注释即可
def plus(num1: int, num2: int) -> int:
    return num1 + num2

# 如果一个函数不返回值,使用None作为返回类型
# 参数的默认值在类型注解后面
def show(value: str, excitement: int = 10) -> None:
    print(value + "!" * excitement)

# 注意,没有类型的参数是动态类型的(被视为Any)
# 而没有任何注解的函数不会被检查
def untyped(x):                                        # 等价于 def untyped(x: Any)
    x.anything() + 1 + "string"      # 没有错误


# 使用Callable注解一个可调用的变量,即函数对象
# 第一个[]中是函数接受的参数,第二个类型式返回值类型
x: Callable[[int, float], float] = f
def register(callback: Callable[[str], int]) -> None: ...

# 一个生成器函数产生ints,其实就是一个返回ints迭代器的函数,因此使用Iterator进行注解
def gen(n: int) -> Iterator[int]:
    i = 0
    while i < n:
        yield i
        i += 1

# 当然,你可以把一个函数注解分成多行
def send_email(address: Union[str, list[str]],
               sender: str,
               cc: Optional[list[str]],
               bcc: Optional[list[str]],
               subject: str = '',
               body: Optional[list[str]] = None
               ) -> bool:
    ...

# 可以为位置参数和关键字参数添加注解
def quux(x: int, /, *, y: int) -> None:
    pass

quux(3, y=5)  # 正确
quux(3, 5)  # 错误:对于"quux"来说,只有一个位置限定参数,即x
quux(x=3, y=5)  # 错误:对于"quux"来说,"x"是意外的关键字参数

# 下面表示每个位置参数和每个关键字参数都是"str"
def call(self, *args: str, **kwargs: str) -> str:
    reveal_type(args)  # 揭示的类型是 "tuple[str, ...]"
    reveal_type(kwargs)  # 揭示的类型是 "dict[str, str]"
    request = make_request(*args, **kwargs)
    return self.do_api_query(request)

3.4 类注解

# 银行账户类
class BankAccount:
    # "__init__" 方法不返回任何内容,所以它和其他不返回任何内容的方法一样,为其注解None的返回类型
    def __init__(self, account_name: str, initial_balance: int = 0) -> None:
        # 补全工具和静态检查工具可以根据参数的类型推断出这些实例变量的正确类型
        self.account_name = account_name
        self.balance = initial_balance

    # 对于实例方法,省略 "self" 的类型
    def deposit(self, amount: int) -> None:
        self.balance += amount

    def withdraw(self, amount: int) -> None:
        self.balance -= amount

# 用户定义的类可以用于注解
account: BankAccount = BankAccount("Alice", 400)
def transfer(src: BankAccount, dst: BankAccount, amount: int) -> None:
    src.withdraw(amount)
    dst.deposit(amount)

# 审计账户是银行账户的派生类
class AuditedBankAccount(BankAccount):
    # 可以在类中为类属性进行注解
    audit_log: list[str]

    def __init__(self, account_name: str, initial_balance: int = 0) -> None:
        super().__init__(account_name, initial_balance)
        self.audit_log: list[str] = []

    def deposit(self, amount: int) -> None:
        self.audit_log.append(f"Deposited {amount}")
        self.balance += amount

    def withdraw(self, amount: int) -> None:
        self.audit_log.append(f"Withdrew {amount}")
        self.balance -= amount

audited = AuditedBankAccount("Bob", 300)
# 补全工具和代码检查工具可以接受鸭子类型,即接受 BankAccount 的函数也接受 BankAccount 的任何子类
transfer(audited, account, 100)  # 类型检查通过!

# 可以使用 ClassVar 来注解一个类属性
class Car:
    seats: ClassVar[int] = 4
    passengers: ClassVar[list[str]]

# 如果你想在你的类上有动态属性,则需要重写 "__setattr__" 或 "__getattr__"
class A:
    # 这将允许给 A.x 赋值,代码检查中会要求 A.x 的类型和value的类型一致
    # (使用 "value: Any" 允许任意类型)
    def __setattr__(self, name: str, value: int) -> None: ...

    # 这将允许访问 A.x,代码检查中会要求 A.x 的类型和返回值的类型一致
    def __getattr__(self, name: str) -> int: ...

a.foo = 42  # 正常工作
a.bar = 'Ex-parrot'  # 类型检查失败

3.5 和静态检查工具以及补全工具交互

可以使用一些特定的约定和静态检查工具和补全工具进行交互

from typing import Union, Any, Optional, TYPE_CHECKING, cast

# 写代码的时候如果想知道某个表达式,可以让代码补全工具或者静态检查工具给出他推断出的类型,即把表达式包裹在reveal_type()中。
# 静态检查工具和补全工具将打印一个带有类型的错误消息,在运行代码之前再删除它。
reveal_type(1)  # 类型是 "builtins.int"

# 如果你用一个空容器或 "None" 初始化一个变量,就要显式的进行类型注解来帮助静态检查工具和补全工具
x: list[str] = []
x: Optional[str] = None

def mystery_function():
    # Magic happens here
    ...

# 如果你不知道某个变量的类型,或者它太动态以至于很难写出一个类型,就使用 Any
x: Any = mystery_function()
# 这样对变量进行任何操作静态检查工具和补全工具都不会报错!
x.whatever() * x["you"] + x("want") - any(x) and all(x) is super  # 没有错误

# 当补全工具或者静态检查工具怀疑你有错误但是你确定这里是没问题的时候,可以使用 "type: ignore" 注释来忽略某个错误,
# PS:最好添加一个解释的注释。
x = confusing_function()  # type: ignore  # confusing_function在这里不会返回None,因为...

# "cast" 是一个辅助函数,它可以强制让静态检查工具和补全工具使用你提供的类型。它只用于类型检查,在运行时会被忽略
a = [4]                                    # a的类型是 list[int]
b = cast(list[int], a)  # 让 b = a 的同时注解 b 的类型是 list[int]
c = cast(list[str], a)  # 让 c = a 的同时注解 c 的类型是 list[str],但真正在运行的时候还是list[int]
reveal_type(c)  # 类型是 "builtins.list[builtins.str]"
print(c)  # 仍然打印 [4] ... 在运行时对象没有改变或被转型

# 使用 "TYPE_CHECKING"来指定静态检查工具检查时运行的代码,这些代码不会在运行时执行
if TYPE_CHECKING:
    import json
else:
    import orjson as json  # 代码检查时不会运行这句话

4. Type Hint中级

4.1 容器类型注解

typing模块中定义的Mapping, MutableMapping, Sequence, Iterable等都是抽象基类(ABCs, Abstract Base Classes),它们为Python的内置容器类型提供了一种形式化的方式来描述其行为。这些抽象基类定义了一组方法,任何实现了这些方法的类都可以被认为是这个抽象基类的子类。

  1. IterableIterable是最基本的抽象基类,代表了所有可以迭代的对象。任何定义了__iter__方法的对象都可以被认为是Iterable。这包括所有的序列类型(如liststrtuple),也包括非序列类型(如dictset)。
  2. SequenceSequenceIterable的子类,代表了所有的序列类型。除了要求实现__iter__方法,Sequence还要求实现__len____getitem__方法。
  3. MappingMapping也是Iterable的子类,代表了所有的映射类型(如dict)。Mapping要求实现__iter____len____getitem__方法,同时还要求实现keysitemsvaluesget__contains__,和__eq__方法。
  4. MutableMappingMutableMappingMapping的子类,代表了所有可变的映射类型。除了Mapping的所有方法,MutableMapping还要求实现__setitem____delitem__poppopitemclearupdatesetdefault等方法。

这些抽象基类在类型注解中非常有用,因为它们让我们可以描述一个对象应该具有的行为,而不是对象的具体类型。例如,如果一个函数接受一个Iterable参数,那么我们可以传入任何可迭代的对象,不论是liststrtuple,还是dictset等。

from typing import Mapping, MutableMapping, Sequence, Iterable

# 接受支持__iter__方法的对象作为参数的函数,可以使用 Iterable 进行注释
# 接受支持__iter__、__len__ 和 __getitem__方法的对象作为参数的函数,使用 Sequence 进行注释
def f(ints: Iterable[int]) -> list[str]:
    return [str(x) for x in ints]

f(range(1, 3))

# Mapping 描述了一个我们不会改变的类似字典的对象,即在 Iterable 的基础上由 __getitem__ 方法
# MutableMapping 描述了我们可能会改变的对象,即在 Mapping 的基础上实现了 __setitem__ 方法
def f(my_mapping: Mapping[int, str]) -> list[int]:
    my_mapping[5] = 'maybe'  # 尝试设置 Mapping 类型,静态检查工具会报错
    return list(my_mapping.keys())

f({3: 'yes', 4: 'no'})

def f(my_mapping: MutableMapping[int, str]) -> set[str]:
    my_mapping[5] = 'maybe'  # 设置 MutableMapping 是没问题的
    return set(my_mapping.values())

f({3: 'yes', 4: 'no'})

import sys
from typing import IO

# 对于应该接受或返回 open() 调用返回的对象的函数,使用 IO[str] 或 IO[bytes]
# (注意 IO 没有区分读取,写入或其他模式)
def get_sys_IO(mode: str = 'w') -> IO[str]:
    if mode == 'w':
        return sys.stdout
    elif mode == 'r':
        return sys.stdin
    else:
        return sys.stdout

4.2 前向注解

# 你可能需要在类定义之前就引用它。
# 这被称为"前向引用"。
def f(foo: A) -> int:  # 在运行时会失败,因为'A'没有被定义
    ...

# 然而,如果你添加了以下特殊的导入:
from __future__ import annotations
# 在运行时它会工作,只要文件后面有一个同名的类,类型检查也会成功
def f(foo: A) -> int:  # 正常
    ...

# 另一个选择就是把类型放在引号里
def f(foo: 'A') -> int:  # 也是正常的
    ...

class A:
    # 如果你需要在类的定义内部的类型注解中引用该类,也可能会遇到这种情况
    @classmethod
    def create(cls) -> A:
        ...

4.3 类型别名 TypeAlias

在某些情况下,类型注解可能会变得很长并且难以输入:

def f() -> Union[list[dict[tuple[int, str], set[int]]], tuple[str, list[str]]]:
    ...

当遇到这样的情况时,在早期版本可以将类型注解赋值给一个变量来定义一个类型别名:

AliasType = Union[list[dict[tuple[int, str], set[int]]], tuple[str, list[str]]]

# 现在我们可以在代替完整名称的地方使用AliasType:

def f() -> AliasType:
    ...

注意:类型别名并不会创建一个新的类型。它只是另一种类型的简写表示 - 它等同于目标类型,除了泛型别名。

在PEP 613中引入的了类型别名,显示的支持了对类型别名变量的注解。这样显式注解类型别名是可以提高可读性:

from typing import TypeAlias  # 在Python 3.9及更早版本中使用 "from typing_extensions"

AliasType: TypeAlias = Union[list[dict[tuple[int, str], set[int]]], tuple[str, list[str]]]

文章作者: Jack Wang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Jack Wang !
  目录