跳到内容

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_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_uppercasecapitalize

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

您输入函数的第一个参数,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_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+”。

列表 (List)

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

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

作为类型,放入 list

由于列表是一种包含某些内部类型的类型,因此您将它们放在方括号内

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

信息

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

在这种情况下,str 是传递给 list 的类型参数。

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

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

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

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

尽管如此,编辑器知道它是一个 str,并为此提供支持。

元组 (Tuple) 和集合 (Set)

您将以相同的方式声明 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)

要定义一个 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)

这意味着

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

联合 (Union)

您可以声明一个变量可以是几种类型之一,例如 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}!")
🤓 其他版本和变体
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}!")

然后您就不必担心像 OptionalUnion 这样的名称了。 😎

泛型类型

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

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

  • 列表 (list)
  • 元组 (tuple)
  • 集合 (set)
  • 字典 (dict)

并且与之前的 Python 版本一样,从 typing 模块导入

  • 联合 (Union)
  • 可选 (Optional)
  • ……等等。

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

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

  • 列表 (list)
  • 元组 (tuple)
  • 集合 (set)
  • 字典 (dict)

以及来自 typing 模块的泛型

  • 联合 (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_personPerson 类的实例”。

这并不表示“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

信息

要了解更多关于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}"

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

但是,您可以使用 Annotated 中的这个空间来为FastAPI提供有关您希望应用程序如何行为的附加元数据。

要记住的关键是,您传递给 Annotated第一个类型参数实际类型。其余的只是其他工具的元数据。

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

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

提示

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

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

FastAPI 中的类型提示

FastAPI 利用这些类型提示来执行多项操作。

使用FastAPI,您用类型提示声明参数,您将获得

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

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

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

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

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

信息

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