跳转至

Python 类型简介

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

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

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

这只是一个关于 Python 类型提示的快速教程/复习。它仅涵盖了与 FastAPI 一起使用所需的最低要求……实际上非常少。

FastAPI 完全基于这些类型提示,它们赋予它许多优势和好处。

但即使您从不使用 FastAPI,学习一些关于它们的知识也会让您受益。

Note

如果您是 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 吗?是 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

带有类型参数的泛型类型

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

这些具有内部类型的类型称为“泛型”类型。并且可以声明它们,甚至包括它们的内部类型。

要声明这些类型及其内部类型,您可以使用标准的 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+”。

列表

例如,让我们定义一个变量为 strlist

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

作为类型,放入 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)

Info

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

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

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

Tip

如果您使用 Python 3.9 或更高版本,您不必从 typing 导入 List,您可以使用相同的常规 list 类型代替。

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

没有类型,这几乎是不可能实现的。

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

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

元组和集合

您会做同样的事情来声明 tupleset

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

联合

您可以声明一个变量可以是几种类型中的任何一种,例如,intstr

在 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

如果您使用的 Python 版本低于 3.10,这里有一个来自我非常主观的观点的提示:

  • 🚨 避免使用 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 这样的名称了。😎

泛型类型

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

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

  • list
  • tuple
  • set
  • dict

并且与 Python 3.8 相同,来自 typing 模块:

  • Union
  • Optional(与 Python 3.8 相同)
  • ……以及其他。

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

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

  • list
  • tuple
  • set
  • dict

并且与 Python 3.8 相同,来自 typing 模块:

  • Union
  • Optional
  • ……以及其他。
  • List
  • Tuple
  • Set
  • Dict
  • Union
  • 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

Info

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

FastAPI 完全基于 Pydantic。

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

Tip

当您使用 OptionalUnion[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。😎

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

Tip

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

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

FastAPI 中的类型提示

FastAPI 利用这些类型提示来做几件事。

使用 FastAPI,您使用类型提示声明参数,然后您获得:

  • 编辑器支持
  • 类型检查

……并且 FastAPI 使用相同的声明来:

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

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

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

Info

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