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
因为编辑器知道变量的类型,你不仅得到了补全,还得到了错误检查:
现在你知道你必须修复它,将 age
转换为字符串 str(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
,你需要传递两个用逗号分隔的类型参数。
第一个类型参数用于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
¶
如果你使用的 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}!")
参数 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}!")
然后你就不必担心像 Optional
和 Union
这样的名字了。😎
泛型类型¶
这些在方括号中接受类型参数的类型被称为**泛型类型**或**泛型**,例如:
你可以使用相同的内置类型作为泛型(带有方括号和内部类型):
list
tuple
set
dict
与 Python 3.8 一样,从 typing
模块中:
Union
Optional
(与 Python 3.8 相同)- ...以及其他。
在 Python 3.10 中,作为使用泛型 Union
和 Optional
的替代方法,你可以使用 竖线 (|
) 来声明类型的联合,这要好得多,也更简单。
你可以使用相同的内置类型作为泛型(带有方括号和内部类型):
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
当你在没有默认值的情况下使用 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。😎
稍后你将看到它有多么 强大。
Tip
这个事实是 标准 Python 意味着你仍然会在你的编辑器中获得 最佳的开发者体验,使用你用来分析和重构代码的工具等等。✨
而且你的代码将与许多其他 Python 工具和库非常兼容。🚀
FastAPI 中的类型提示¶
FastAPI 利用这些类型提示来做几件事情。
使用 FastAPI,你用类型提示声明参数,你会得到:
- 编辑器支持。
- 类型检查。
...而 FastAPI 使用相同的声明来:
- 定义需求:从请求路径参数、查询参数、头部、主体、依赖项等。
- 转换数据:从请求到所需类型。
- 验证数据:来自每个请求:
- 当数据无效时生成 自动错误 返回给客户端。
- 使用 OpenAPI 文档化 API:
- 然后由自动交互式文档用户界面使用。
这听起来可能很抽象。别担心。你会在 教程 - 用户指南 中看到所有这些的实际操作。
重要的是,通过使用标准 Python 类型,在一个地方(而不是添加更多类、装饰器等),FastAPI 会为你做很多工作。
Info
如果你已经完成了整个教程并回来查看更多关于类型的内容,一个很好的资源是 mypy
的“速查表”。