跳到内容

Python 类型简介

Python 支持可选的“类型提示”(也称为“类型注解”)。

这些**“类型提示”**或注解是一种特殊的语法,允许声明变量的类型

通过为变量声明类型,编辑器和工具可以为您提供更好的支持。

这只是关于 Python 类型提示的**快速教程/复习**。它只涵盖了在 **FastAPI** 中使用它们所需的最少内容……这实际上非常简单。

**FastAPI** 完全基于这些类型提示,它们带来了许多优势和好处。

但即使您从未使用过 **FastAPI**,学习一些关于它们的内容也会受益匪浅。

注意

如果您是 Python 专家,并且已经了解所有类型提示,请跳到下一章。

动机

让我们从一个简单的例子开始

def get_full_name(first_name, last_name):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

运行此程序将输出

John Doe

该函数执行以下操作

  • 接受 `first_name` 和 `last_name`。
  • 使用 `title()` 将每个单词的首字母转换为大写。
  • 用空格将它们连接起来。
def get_full_name(first_name, last_name):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

编辑它

这是一个非常简单的程序。

但现在想象一下,您正在从头开始编写它。

在某个时候,您已经开始了函数的定义,参数也已准备就绪……

但接着您必须调用“将首字母转换为大写的方法”。

是 `upper` 吗?是 `uppercase` 吗?`first_uppercase` 吗?`capitalize` 吗?

然后,您尝试使用程序员的老朋友,编辑器自动补全。

您输入函数的第一个参数 `first_name`,然后是一个点(`.`),然后按 `Ctrl+Space` 触发补全。

但是,很遗憾,您没有得到任何有用的结果

添加类型

让我们修改上一版本中的一行。

我们将精确地改变这部分,即函数的参数,从

    first_name, last_name

    first_name: str, last_name: str

就这样。

这些就是“类型提示”

def get_full_name(first_name: str, last_name: str):
    full_name = first_name.title() + " " + last_name.title()
    return full_name


print(get_full_name("john", "doe"))

这与声明默认值(例如)不同

    first_name="john", last_name="doe"

这是不同的东西。

我们使用的是冒号(`:`),而不是等号(`=`)。

并且添加类型提示通常不会改变程序的行为,与不添加类型提示时相同。

但现在,想象一下您又在创建那个函数的过程中,但是这次带有类型提示。

在同一点,您尝试用 `Ctrl+Space` 触发自动补全,然后您会看到

有了它,您可以滚动查看选项,直到找到那个“耳熟能详”的。

更多动机

检查这个函数,它已经有类型提示了

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + age
    return name_with_age

因为编辑器知道变量的类型,您不仅可以获得补全,还可以获得错误检查

现在您知道必须修复它,使用 `str(age)` 将 `age` 转换为字符串

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + str(age)
    return name_with_age

声明类型

您刚刚看到了声明类型提示的主要位置:作为函数参数。

这也是您在 **FastAPI** 中使用它们的主要位置。

简单类型

您可以声明所有标准的 Python 类型,而不仅仅是 `str`。

例如,您可以使用

  • int
  • float
  • bool
  • bytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
    return item_a, item_b, item_c, item_d, item_d, item_e

带类型参数的泛型

有些数据结构可以包含其他值,如 `dict`、`list`、`set` 和 `tuple`。内部的值也可以有自己的类型。

这些具有内部类型的类型被称为“**泛型**”类型。并且可以声明它们,即使是带有它们的内部类型。

要声明这些类型和内部类型,可以使用标准的 Python 模块 `typing`。它专门用于支持这些类型提示。

Python 的新版本

使用 `typing` 的语法**兼容**所有版本,从 Python 3.6 到最新版本,包括 Python 3.9、Python 3.10 等。

随着 Python 的发展,**新版本**对这些类型注解提供了改进的支持,在许多情况下,您甚至不需要导入和使用 `typing` 模块来声明类型注解。

如果您的项目可以选择使用较新版本的 Python,您将能够利用这种额外的简洁性。

在所有文档中,都有与每个 Python 版本兼容的示例(当存在差异时)。

例如,“**Python 3.6+**”表示它兼容 Python 3.6 或更高版本(包括 3.7、3.8、3.9、3.10 等)。而“**Python 3.9+**”表示它兼容 Python 3.9 或更高版本(包括 3.10 等)。

如果您可以使用 **最新版本的 Python**,请使用最新版本的示例,这些示例将具有**最佳和最简单的语法**,例如,“**Python 3.10+**”。

列表

例如,让我们定义一个变量为 `str` 类型的 `list`。

使用相同的冒号(:)语法声明变量。

类型写为 list

由于列表是一种包含内部类型的类型,您将其放在方括号中

def process_items(items: list[str]):
    for item in items:
        print(item)

从 `typing` 中导入 `List`(大写 `L`)

from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)

使用相同的冒号(:)语法声明变量。

类型写为您从 `typing` 导入的 `List`。

由于列表是一种包含内部类型的类型,您将其放在方括号中

from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)

信息

方括号中的这些内部类型被称为“类型参数”。

在这种情况下,`str` 是传递给 `List`(或 Python 3.9 及以上版本的 `list`)的类型参数。

这意味着:“变量 `items` 是一个 `list`,并且此列表中的每个项都是一个 `str`”。

提示

如果您使用 Python 3.9 或更高版本,则无需从 `typing` 导入 `List`,而可以直接使用常规的 `list` 类型。

通过这样做,您的编辑器甚至在处理列表中的项目时也能提供支持

如果没有类型,这几乎不可能实现。

请注意,变量 `item` 是列表 `items` 中的一个元素。

然而,编辑器仍然知道它是一个 `str`,并为此提供了支持。

元组和集合

您也可以用同样的方法声明 `tuple` 和 `set`

def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
    return items_t, items_s
from typing import Set, Tuple


def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]):
    return items_t, items_s

这意味着

  • 变量 `items_t` 是一个包含 3 个项的 `tuple`,一个 `int`,另一个 `int`,以及一个 `str`。
  • 变量 `items_s` 是一个 `set`,其每个项的类型都是 `bytes`。

字典

要定义一个 `dict`,您需要传递 2 个类型参数,用逗号分隔。

第一个类型参数用于 `dict` 的键。

第二个类型参数用于 `dict` 的值

def process_items(prices: dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)
from typing import Dict


def process_items(prices: Dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)

这意味着

  • 变量 `prices` 是一个 `dict`
    • 此 `dict` 的键的类型是 `str`(例如,每个项的名称)。
    • 此 `dict` 的值的类型是 `float`(例如,每个项的价格)。

联合

您可以声明一个变量可以是**多种类型**中的任何一种,例如,`int` 或 `str`。

在 Python 3.6 及以上版本(包括 Python 3.10)中,您可以使用 `typing` 模块中的 `Union` 类型,并在方括号中放入可接受的类型。

在 Python 3.10 中,还有一种**新语法**,您可以用竖线(|分隔可能的类型。

def process_item(item: int | str):
    print(item)
from typing import Union


def process_item(item: Union[int, str]):
    print(item)

在两种情况下,这都意味着 `item` 可以是 `int` 或 `str`。

可能为 `None`

您可以声明一个值可以具有某种类型,如 `str`,但它也可以是 `None`。

在 Python 3.6 及以上版本(包括 Python 3.10)中,您可以通过从 `typing` 模块导入并使用 `Optional` 来声明它。

from typing import Optional


def say_hi(name: Optional[str] = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")

使用 `Optional[str]` 而不是仅仅 `str` 将使编辑器帮助您检测错误,在这些错误中,您可能假设一个值总是 `str`,而它实际上也可能是 `None`。

Optional[Something] 实际上是 Union[Something, None] 的快捷方式,它们是等效的。

这也意味着在 Python 3.10 中,您可以使用 Something | None

def say_hi(name: str | None = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")
from typing import Optional


def say_hi(name: Optional[str] = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")
from typing import Union


def say_hi(name: Union[str, None] = None):
    if name is not None:
        print(f"Hey {name}!")
    else:
        print("Hello World")

使用 `Union` 或 `Optional`

如果您正在使用低于 3.10 的 Python 版本,这里是我**非常主观**的观点提供的一个提示

  • 🚨 避免使用 `Optional[SomeType]`
  • 而是 ✨ **使用 `Union[SomeType, None]`** ✨。

两者是等效的,底层原理相同,但我会推荐 `Union` 而不是 `Optional`,因为“**可选**”这个词似乎暗示该值是可选的,而它实际上意味着“它可以是 `None`”,即使它不是可选的并且仍然是必需的。

我认为 `Union[SomeType, None]` 对其含义更明确。

这只是关于词语和名称的问题。但这些词语会影响您和您的队友如何思考代码。

举个例子,我们来看这个函数

from typing import Optional


def say_hi(name: Optional[str]):
    print(f"Hey {name}!")
🤓 其他版本和变体
def say_hi(name: str | None):
    print(f"Hey {name}!")

参数 `name` 定义为 `Optional[str]`,但它**不是可选的**,您不能在没有该参数的情况下调用函数

say_hi()  # Oh, no, this throws an error! 😱

`name` 参数**仍然是必需的**(而不是*可选的*),因为它没有默认值。但是,`name` 接受 `None` 作为值

say_hi(name=None)  # This works, None is valid 🎉

好消息是,一旦您使用 Python 3.10,就不必担心这个问题了,因为您将能够简单地使用 `|` 来定义类型联合

def say_hi(name: str | None):
    print(f"Hey {name}!")
🤓 其他版本和变体
from typing import Optional


def say_hi(name: Optional[str]):
    print(f"Hey {name}!")

然后您就不必担心 `Optional` 和 `Union` 这样的名称了。😎

泛型

这些在方括号中接受类型参数的类型称为**泛型类型**或**泛型**,例如

您可以使用相同的内置类型作为泛型(带有方括号和内部类型)

  • list
  • tuple
  • set
  • dict

以及与 Python 3.8 相同,来自 `typing` 模块

  • 联合
  • `Optional`(与 Python 3.8 相同)
  • ……等等。

在 Python 3.10 中,作为使用泛型 `Union` 和 `Optional` 的替代方案,您可以使用竖线(|来声明类型联合,这会好得多且更简单。

您可以使用相同的内置类型作为泛型(带有方括号和内部类型)

  • list
  • tuple
  • set
  • dict

以及与 Python 3.8 相同,来自 `typing` 模块

  • 联合
  • Optional
  • ……等等。
  • 列表
  • Tuple
  • Set
  • 字典
  • 联合
  • Optional
  • ……等等。

类作为类型

您也可以将一个类声明为变量的类型。

假设您有一个名为 `Person` 的类

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


def get_person_name(one_person: Person):
    return one_person.name

然后您可以声明一个变量为 `Person` 类型

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


def get_person_name(one_person: Person):
    return one_person.name

然后,您再次获得所有的编辑器支持

请注意,这表示“one_person 是类 Person 的一个**实例**”。

这不表示“one_person 是名为 Person 的**类**”。

Pydantic 模型

Pydantic 是一个用于执行数据验证的 Python 库。

您将数据的“形状”声明为带有属性的类。

每个属性都有一个类型。

然后,您使用一些值创建该类的一个实例,它将验证这些值,将它们转换为适当的类型(如果适用),并为您提供一个包含所有数据的对象。

通过该结果对象,您将获得所有的编辑器支持。

Pydantic 官方文档中的一个示例

from datetime import datetime

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: datetime | None = None
    friends: list[int] = []


external_data = {
    "id": "123",
    "signup_ts": "2017-06-01 12:22",
    "friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import Union

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: Union[datetime, None] = None
    friends: list[int] = []


external_data = {
    "id": "123",
    "signup_ts": "2017-06-01 12:22",
    "friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import List, Union

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = "John Doe"
    signup_ts: Union[datetime, None] = None
    friends: List[int] = []


external_data = {
    "id": "123",
    "signup_ts": "2017-06-01 12:22",
    "friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123

信息

要了解更多关于 Pydantic 的信息,请查阅其文档

**FastAPI** 完全基于 Pydantic。

您将在教程 - 用户指南中看到所有这些的更多实际应用。

提示

当您在不带默认值的情况下使用 `Optional` 或 `Union[Something, None]` 时,Pydantic 会有特殊的行为,您可以在 Pydantic 文档中关于必需可选字段的部分阅读更多内容。

带元数据注解的类型提示

Python 还有一个特性,允许使用 `Annotated` 在这些类型提示中添加**额外的元数据**。

在 Python 3.9 中,`Annotated` 是标准库的一部分,因此您可以从 `typing` 导入它。

from typing import Annotated


def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
    return f"Hello {name}"

在 Python 3.9 以下的版本中,您从 `typing_extensions` 导入 `Annotated`。

它将随 **FastAPI** 一起安装。

from typing_extensions import Annotated


def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
    return f"Hello {name}"

Python 本身不会对这个 `Annotated` 做任何事情。对于编辑器和其他工具,类型仍然是 `str`。

但您可以使用 `Annotated` 中的这个空间来为 **FastAPI** 提供关于您希望应用程序如何行为的额外元数据。

需要记住的重要一点是,传递给 `Annotated` 的**第一个*类型参数***是**实际类型**。其余的只是其他工具的元数据。

目前,您只需要知道 `Annotated` 存在,并且它是标准的 Python 特性。😎

稍后您将看到它有多么**强大**。

提示

这是**标准 Python** 的事实意味着您仍然可以在编辑器中获得**最佳的开发体验**,使用您用来分析和重构代码的工具等。✨

并且您的代码也将与许多其他 Python 工具和库高度兼容。🚀

**FastAPI** 中的类型提示

**FastAPI** 利用这些类型提示来完成多项工作。

使用 **FastAPI**,您可以用类型提示声明参数,并获得

  • 编辑器支持.
  • 类型检查.

……而且 **FastAPI** 使用相同的声明来

  • **定义要求**:来自请求路径参数、查询参数、请求头、请求体、依赖项等。
  • **转换数据**:将请求中的数据转换为所需类型。
  • **验证数据**:来自每个请求的数据
    • 当数据无效时,生成并向客户端返回**自动错误**。
  • 使用 OpenAPI **记录** API
    • 然后由自动交互式文档用户界面使用。

这听起来可能有点抽象。别担心。您将在教程 - 用户指南中看到所有这些的实际应用。

重要的是,通过在一个地方使用标准 Python 类型(而不是添加更多类、装饰器等),**FastAPI** 将为您完成大量工作。

信息

如果您已经完成了所有教程并回来查看更多关于类型的内容,一个很好的资源是 mypy 的“备忘单”