Python 3.11 你应该试试的超酷特性:四、Type hints新增类型及语法

522 阅读5分钟

Python 3.11 你应该试试的超酷特性

Python 3.11在10月24日发布。它是Python最新版本,运行速度更快且更佳友好。在经过17个月的研发,终于到了可以使用的黄金时期。

和每个发布版本一样,Python 3.11做了大量的变更及提高。你可以通过查看文档来了解大部分内容。这里,我们会阐述最酷且最有影响力的新特性。

教程里将涵盖如下内容:

  • 更好的错误信息来帮助代码追踪
  • Faster CPython项目加速代码执行
  • 在异步代码中使用Task和exception groups
  • Python静态类型新增的几个类型特性
  • 原生支持TOML格式配置文件

如果你想运行本教程所提供的示例文件,你需要先安装Python 3.11.

Type hints新增类型及语法

Python为动态类型语言,不过它提供了可选类型提示(type hint)。Python静态类型系统创建于2015年,定义在PEP 484。从Python 3.5开始,Python每个版本的发布都会引入一些新的类型相关提案。

Python 3.11发布了五个类型相关的PEP:

本节,我们会介绍其中的两项:可变参数泛型(variadic generics)和Self类型,Python 3.11文档查看更多相关内容。

Python静态类型系统诞生时就有了类型变量。使用它们实现参数化的泛型。通俗来说,如果你有个类型,你可以使用类型变量来检查列表内部的元素类型:

from typing import Sequence, TypeVar

T = TypeVar("T")

def first(sequence: Sequence[T]) -> T:
    return sequence[0]

first()函数接收一个序列类型参数并返回第一个元素,参数可以是列表。该代码在序列里元素类型不同时同样正常工作。我们在first()返回值需要指明其返回为相同输入元素类型。

类型变量就是实现该功能的。例如,如果传递给first()列表元素为整型,那么T在执行期间就会置为整型。type checker就可以推断出first()返回为整型。本例子中,列表视为泛型,因为它可以通过其它类型实现参数化。

有一个场景就是通过type hint来指示该类型为当前类,我们继续来看看前面的Person类:

# programmers.py

from dataclasses import dataclass

# ...

@dataclass
class Person:
    name: str
    life_span: tuple[int, int]

    @classmethod
    def from_dict(cls, info):
        return cls(
            name=f"{info['name']['first']} {info['name']['last']}",
            life_span=(info["birth"]["year"], info["death"]["year"]),
        )

.from_dict()构造器返回一个Person对象。然而,这里.from_dict()里不能指定返回类型为-> Person,因为在当时Person类并没有定义完全。

一个解决办法是使用类型变量来绑定类:

# programmers.py

# ...

from typing import Any, Type, TypeVar

TPerson = TypeVar("TPerson", bound="Person")

@dataclass
class Person:
    name: str
    life_span: tuple[int, int]

    @classmethod
    def from_dict(cls: Type[TPerson], info: dict[str, Any]) -> TPerson:
        return cls(
            name=f"{info['name']['first']} {info['name']['last']}",
            life_span=(info["birth"]["year"], info["death"]["year"]),
        )

我们使用bound来指定TPerson为Person或其子类。该方式可是可以,但是却不好读。

现在我们使用Self类型来实现了。它指明其为包装的类,我们不再需要手动去定义类型变量。代码如下:

# programmers.py

# ...

from typing import Any, Self

@dataclass
class Person:
    name: str
    life_span: tuple[int, int]

    @classmethod
    def from_dict(cls, info: dict[str, Any]) -> Self:
        return cls(
            name=f"{info['name']['first']} {info['name']['last']}",
            life_span=(info["birth"]["year"], info["death"]["year"]),
        )

查看PEP 673可以了解更多详细内容。

类型变量有个限制就是每个类型只能对应一个对象。假设你有个函数返回两个元素的tuple类型。

# pair_order.py


def flip(pair):
    first, second = pair
    return (second, first)

这假定pair返回两个元素的tuple,元素可以具备不同类型,所有我们需要两个类型变量来对函数进行注解

# pair_order.py

from typing import TypeVar


T0 = TypeVar("T0")
T1 = TypeVar("T1")


def flip(pair: tuple[T0, T1]) -> tuple[T1, T0]:
    first, second = pair
    return (second, first)

这里显得稍微笨重了点,不过也还好。注解意图还是比较清晰易于理解,比较有难度的是当如果tuple具有任意数量时这里就难办了。

# tuple_order.py


def cycle(elements):
    first, *rest = elements
    return (*rest, first)

cycle()函数将任意长度的tuple第一个元素移至末尾,如果传入的一对元素那么等同于flip()。

想想我们该如何对cycle()注解。如果elemnts是一个包含n个元素的tuple,我们就需要定义n个类型变量。而且这些任意元素可以是任意类型,所以我们不知道需要多少类型变量。

PEP 464引入了TypeVarTuple来处理这种场景。一个TypeVarTuple可以用来针对任意类型元素。因此可以利用可变参数的泛型类型来进行注解。

我们通过如下实现cycle()的type hints:

# tuple_order.py

from typing import TypeVar, TypeVarTuple

T0 = TypeVar("T0")
Ts = TypeVarTuple("Ts")


def cycle(elements: tuple[T0, *Ts]) -> tuple[*Ts, T0]:
    first, *rest = elements
    return (*rest, first)

TypeVarTuple将会替换为任意数量类型,所以该注解支持1、3、7或者任意数量元素。

注意在Ts前面的星号(*)是语法的一部分,它和解包语法类似,也可以提醒我们该类型为可变数量类型。

Python3.10要使用该功能需要安装typing_extension,还需要用Unpack[Ts]来替代*Ts语法