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}!")
参数 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}!")
然后你就不必担心 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
模块。
联合
可选
- ...以及其他。
列表
元组
集合
字典
联合
可选
- ...以及其他。
类作为类型¶
你也可以将一个类声明为变量的类型。
假设你有一个类 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
的“备忘单”。