跳转至

Python 类型提示简介

🌐 Translation by AI and humans

This translation was made by AI guided by humans. 🤝

It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖

You can improve this translation by helping us guide the AI LLM better.

English version

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"))
🤓 Other versions and variants
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"))
🤓 Other versions and variants
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"))
🤓 Other versions and variants
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
🤓 Other versions and variants
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
🤓 Other versions and variants
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
🤓 Other versions and variants
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

带类型参数的泛型类型

有些数据结构可以包含其他值,比如 dictlistsettuple。而内部的值也会有自己的类型。

这些带有内部类型的类型称为“泛型”(generic)类型。可以把它们连同内部类型一起声明出来。

要声明这些类型以及内部类型,你可以使用 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

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

def process_items(items: list[str]):
    for item in items:
        print(item)
🤓 Other versions and variants
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
🤓 Other versions and variants
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)
🤓 Other versions and variants
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

在 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 可以是 intstr

可能为 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")

使用 UnionOptional

如果你使用的是 3.10 以下的 Python 版本,这里有个来自我非常主观的建议:

  • 🚨 避免使用 Optional[SomeType]
  • 改用 ✨Union[SomeType, None]

两者等价,底层相同,但我更推荐 Union 而不是 Optional,因为“optional(可选)”这个词看起来像是在说“参数可选”,而它实际上表示“它可以是 None”,即使它并不是可选的,仍然是必填的。

我认为 Union[SomeType, None] 更明确地表达了它的意思。

这只是关于词语和名字。但这些词会影响你和你的队友如何理解代码。

例如,看下面这个函数:

from typing import Optional


def say_hi(name: Optional[str]):
    print(f"Hey {name}!")
🤓 Other versions and variants
def say_hi(name: str | None):
    print(f"Hey {name}!")

参数 name 被定义为 Optional[str],但它并不是“可选”的,你不能不传这个参数就调用函数:

say_hi()  # 哦不,这会抛错!😱

参数 name 仍然是必填的(不是“可选”),因为它没有默认值。不过,name 接受 None 作为值:

say_hi(name=None)  # 这样可以,None 是有效值 🎉

好消息是,一旦你使用 Python 3.10,就无需再为此操心,因为你可以直接用 | 来定义类型联合:

def say_hi(name: str | None):
    print(f"Hey {name}!")
🤓 Other versions and variants
from typing import Optional


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

这样你就不必再考虑 OptionalUnion 这些名字了。😎

泛型类型

这些在方括号中接收类型参数的类型称为“泛型类型”(Generic types)或“泛型”(Generics),例如:

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

  • list
  • tuple
  • set
  • dict

以及与之前的 Python 版本一样,来自 typing 模块的:

  • Union
  • Optional
  • ……以及其他。

在 Python 3.10 中,作为使用泛型 UnionOptional 的替代,你可以使用竖线(|来声明类型联合,这更好也更简单。

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

  • list
  • tuple
  • set
  • dict

以及来自 typing 模块的泛型:

  • Union
  • Optional
  • ……以及其他。

类作为类型

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

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

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


def get_person_name(one_person: Person):
    return one_person.name
🤓 Other versions and variants
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
🤓 Other versions and variants
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 之上。

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

提示

当你在没有默认值的情况下使用 OptionalUnion[Something, None] 时,Pydantic 有一个特殊行为,你可以在 Pydantic 文档的 必填的 Optional 字段 中了解更多。

带元数据注解的类型提示

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}"
🤓 Other versions and variants
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 的“速查表”