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
的“备忘单”。