Python 类型介绍
Python 是动态语言,但是也支持类型注释,不过在编写 Python 代码时,这种 “类型注释” 不是强制要求的。
这些所谓的 “类型注释” 可以理解为是一种特殊的语法,这些特殊的语法能够声明一个 Python 变量的类型。
编辑器和一些工具能够通过你标注的 “类型注释” ,提供更好的编程过程中的支持,比如代码补全提示等。
接下来要介绍的 “类型注释” 内容只是 Python 类型注释中的冰山一角,仅仅可以覆盖后面在使用Fast API时所必需的内容。
接下来介绍的类型注释是Fast API中所使用到的特性,这些类型注释给Fast API提供了很多优势功能和便利。
不过话说回来,即便你不使用Fast API,你也会通过阅读本文而受益匪浅的。
Note
如果你是一名 Python 专家,并且完全掌握类型注释,请直接跳至下一章。
1. 简单例子
让我们从一些简单的例子开始:
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"))
运行这段代码可以得到打印输出:
get_full_name
做了下面3件事:
- 接收了参数
first_name
和last_name
- 使用
title()
函数,将first_name
和last_name
首字母转为大写 - 将修改后的
first_name
和last_name
通过 空格连接起来
1.1 从头开始
上面是一段非常简单的程序,但是现在想象一下你正在从头编写这段程序代码。当你已经定义完成了函数如下:
现在你开始编写逻辑,你必需将 first_name
和 last_name
首字母转为大写,你突然想不起来转为大写的函数是:upper?
,uppercase?
, first_uppercase?
还是 capitalize?
然后,你想起来了你的老朋友:编辑器的代码补全功能。
你先输入了函数的第一个参数: first_name
,然后输入了一个点 .
然后你会发现,并没有给你提示任何有用的方法。
1.2 添加类型
我们从上面的版本开始,改变一行,把函数的参数从:
改为:
整体看下来如下,参数后面的类型就称为 “类型注释”
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"))
这和下面的声明缺省值是两码事:
我们定义 类型注释 使用的是 :
而不是 =
,并且添加 类型注释 和不添加 类型注释,不会改变函数的任何性质。
现在,想象一下你又一次正在从头编写这段程序代码,但是这一次你写了参数注解,在同样的地方,你会发现,编辑器这一次给了你很好的提示:
这样你就可以找到自己想要使用的方法了。
1.3 其他作用
看一下这个函数,它已经写好了类型注释:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + age
return name_with_age
在这个例子中,因为我们添加了 类型注释,编辑器还可以帮助我们检查语法错误:
所以,这时候你就可以很轻松的进行语法错误的修复:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
2. 声明类型
你刚刚看到的函数参数的类型声明,这是 类型注释 主要会用到的地方,这也是以后使用 Fast API 时最常标注类型的地方。
2.1 简单类型
除了上面例子中的 str 类型可以标注外,所有 python 标准的类型都可以标注,例如:
- 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
2.2 包含子类型的类型
有一些数据结果可以包含其他类型,比如 dict
, list
, set
和 tuple
. 这些类型中的元素可以拥有自己的类型。
为了标注这些包含内置元素的类型,可以使用 python 中的标准库:typing
,专为类型注释而生的库。typing
兼容从 Python 3.6 到最新版本的所有版本,包括 Python 3.9、Python 3.10 等。
2.2.1 新版本类型注解
随着 Python 的发展,新版本对这些类型注解的支持也在不断改进,在许多情况下,你甚至不需要导入和使用 typing
模块来声明类型注解。
在本文档中,会有与 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+"。
2.2.2 List
看例子,我们先定义一个存储 str 子元素的 List 变量。
info
这些方括号中的类型被称为“类型参数”。 在本例子中,str 是传递给 List(或者 list 在 python 3.9+) 的类型参数。
意思是:“在 List(或者 list 在 python 3.9+) 中的每一个变量元素的类型都是 str”
tip
如果你是 python 3.9 或更高的版本,不需要从 typing 中导入 List,内置数据结构 list 和 typing 中的 List 是等效的。
通过这样的编写,你的编辑器甚至可以为你提供 list 中的 item 的类型相关提示:
没有类型注解的话,这几乎是不可能实现的。
2.2.3 Tuple 和 Set
同样去声明 tuple
和 set
:
注解含义:
items_t
是一个包含3个元素的tuple
, 3个元素的类型分别是int
、int
和str
items_s
是一个set
, 其中每个子元素的类型都是bytes
2.2.4 Dict
要定义一个 dict
类型,需要传递两个类型参数,这两个类型参数需要用逗号分隔开。第一个类型参数是定义的 dict
的 key
的类型,第二个类型参数是定义的 dict
的 value
的类型。
注解含义:
prices
变量是一个dict
:prices
中的 key 类型全部是str
prices
中的 value 类型全部是float
2.2.5 Union
union
可以声明一个变量的类型是几种类型中的某一个,例如,可以是 int
或 str
。
在 python 3.6 或更加新的版本中(包括 python 3.10),你可以使用 typing
中的 Union
,把一个变量可能接收的类型全部放入一个方括号中去声明。
在 python 3.10 或更加新的版本中,有一种可以通过分隔符号 |
把可能接收的类型分隔声明的新语法,用于表示和 Union
同等的效果。
这两个示例中都表示变量 item
可能是 int
类型或 str
类型。
2.2.6 Optional
可以通过 Optional
来声明一个变量可以是 None
或者一个确定的类型,比如 str
。
在 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
会让编辑器帮助你检查错误,这种错误是当变量 name
也可能是 None
,但你认为总是 str
类型。
Optional[Something]
其实是 Union[Something, None]
的简写形式,它俩是相等的。这也意味着在 python 3.10 中你可以使用分隔符Something | None
进行代替:
2.2.7 Union
还是Optional
如果你现在使用的python 版本低于 3.10 ,这是我非常主观的一个建议:
- 避免使用
Optional[SomeType]
- 使用
Union[SomeType, None]
代替
虽然它两个是等效的并且底层是想通的,但是我推荐 Union[SomeType, None]
代替 Optional[SomeType]
是因为单词 Optional
似乎暗示变量是可选传递的,不过 Optional
确实是表示 变量可以为 None
,但是并不代表变量不是必需传的。
我认为 Union[SomeType, None]
表述的更加地明确。
这只是单词的名称和含义,但是这些单词能够影响你和你的同事对于代码的潜在看法。
我们以这个函数为例子:
name
参数被定义为 Optional[str]
, 但是 name
不是一个选传的参数,你不可以按照这种方式调用:
name
参数仍然是必需传递的(不是选传的),因为它没有 default 值,不过,name
可以接收 None
:
好消息是,一旦你使用了 python 3.10+ 版本,你就不需要担心这个问题了,因为你可以使用 |
来去定义 Union
:
这样就不用去担心 Optional
和 Union
的单词含义问题了。
2.2.8 泛型类型
这些可以使用方括号的类型被称为泛型类型或者泛型,例如:
你可以使用内置类型作为泛型(配合方括号,定义子类型在里面):
list
tuple
set
dict
和 python 3.8 相同的有(从 typing
中导入):
Union
Optional
- ... 其他的
在 python 3.10 中,作为 Union
和 Optional
的替代,可以使用 |
, 这更加的简单和清晰。
你可以使用内置类型作为泛型(配合方括号,定义子类型在里面):
list
tuple
set
dict
和 python 3.8 相同的有(从 typing
中导入):
Union
Optional
- ... 其他的
{ .annotate }
List
Tuple
Set
Dict
Union
Optional
- ... 其他的
{ .annotate }
2.3 class 作为 类型注解
也可以使用 class 定义的类作为变量的类型注解。
假设你有一个类叫做 Person
,有一个 name
属性:
然后你可以声明一个 Person
类型的变量:
可以发现编辑器依然可以支持提示:
注意:这表示 one_person
是 类 Person
的一个实例。
3. Pydantic 模型
Pydantic 是一个用于数据验证的库。
使用 Pydantic,需要通过声明一个 class 类和类属性(每个类属性都有自己的类型)来表示数据的 "形状"。然后你通过传参的形式实例化这个 class 类,它会自动校验传参的数据是否符合类属性的定义类型,并将传入数据转为合适类型,最后还提供一个包含所有数据的object。