跳转至

Python 类型提示简介

🌐 由 AI 与人类协作翻译

本翻译由人类引导的 AI 生成。🤝

可能存在误解原意或不够自然等问题。🤖

你可以通过帮助我们更好地引导 AI LLM来改进此翻译。

英文版本

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_namelast_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?是 uppercasefirst_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 中使用它们的主要场景。

简单类型

你不仅可以声明 str,还可以声明所有标准的 Python 类型。

例如:

  • 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_e

typing 模块

在一些额外的用例中,你可能需要从标准库的 typing 模块导入内容。比如当你想声明“任意类型”时,可以使用 typing 中的 Any

from typing import Any


def some_function(data: Any):
    print(data)

泛型类型

有些类型可以在方括号中接收“类型参数”(type parameters),用于声明其内部值的类型。比如“字符串列表”可以写为 list[str]

这些能接收类型参数的类型称为“泛型类型”(Generic types)或“泛型”(Generics)。

你可以把相同的内建类型作为泛型使用(带方括号和内部类型):

  • list
  • tuple
  • set
  • dict

列表

例如,我们来定义一个由 str 组成的 list 变量。

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

类型写 list

因为 list 是一种包含内部类型的类型,把内部类型写在方括号里:

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

信息

方括号中的这些内部类型称为“类型参数”(type parameters)。

在这个例子中,str 是传给 list 的类型参数。

这表示:“变量 items 是一个 list,并且列表中的每一个元素都是 str”。

这样,即使是在处理列表中的元素时,编辑器也能给你提供支持:

没有类型的话,这几乎是不可能做到的。

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

即便如此,编辑器仍然知道它是 str,并为此提供支持。

元组和集合

声明 tupleset 的方式类似:

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 个类型参数,用逗号分隔。

第一个类型参数用于字典的键。

第二个类型参数用于字典的值:

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 类型(比如,每个条目的价格)。

Union

你可以声明一个变量可以是若干种类型中的任意一种,比如既可以是 int 也可以是 str

定义时使用竖线(|把两种类型分开。

这称为“联合类型”(union),因为变量可以是这两类类型集合的并集中的任意一个。

def process_item(item: int | str):
    print(item)

这表示 item 可以是 intstr

可能为 None

你可以声明一个值的类型是某种类型(比如 str),但它也可能是 None

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

使用 str | None 而不是仅仅 str,可以让编辑器帮助你发现把值当成总是 str 的错误(实际上它也可能是 None)。

类作为类型

你也可以把类声明为变量的类型。

假设你有一个名为 Person 的类,带有 name:

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 的一个实例(instance)”。

它并不表示“one_person 是名为 Person 的类本身(class)”。

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

信息

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

FastAPI 完全建立在 Pydantic 之上。

你会在教程 - 用户指南中看到更多的实战示例。

带元数据注解的类型提示

Python 还提供了一个特性,可以使用 Annotated 在这些类型提示中放入额外的元数据

你可以从 typing 导入 Annotated

from typing 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 的“速查表”