Skip to content

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"))

运行这段代码可以得到打印输出:

John Doe

get_full_name 做了下面3件事:

  • 接收了参数 first_namelast_name
  • 使用 title() 函数,将 first_namelast_name 首字母转为大写
  • 将修改后的 first_namelast_name 通过 空格 连接起来

1.1 从头开始

上面是一段非常简单的程序,但是现在想象一下你正在从头编写这段程序代码。当你已经定义完成了函数如下:

def get_full_name(first_name, last_name):

现在你开始编写逻辑,你必需将 first_namelast_name 首字母转为大写,你突然想不起来转为大写的函数是:upper?uppercase?, first_uppercase? 还是 capitalize?

然后,你想起来了你的老朋友:编辑器的代码补全功能。

你先输入了函数的第一个参数: first_name,然后输入了一个点 . 然后你会发现,并没有给你提示任何有用的方法。

image-20240102215335469.png

1.2 添加类型

我们从上面的版本开始,改变一行,把函数的参数从:

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"

我们定义 类型注释 使用的是 : 而不是 = ,并且添加 类型注释 和不添加 类型注释,不会改变函数的任何性质。

现在,想象一下你又一次正在从头编写这段程序代码,但是这一次你写了参数注解,在同样的地方,你会发现,编辑器这一次给了你很好的提示:

image-20240102220427383

这样你就可以找到自己想要使用的方法了。

1.3 其他作用

看一下这个函数,它已经写好了类型注释:

def get_name_with_age(name: str, age: int):
    name_with_age = name + " is this old: " + age
    return name_with_age

在这个例子中,因为我们添加了 类型注释,编辑器还可以帮助我们检查语法错误:

image-20240102221012885

所以,这时候你就可以很轻松的进行语法错误的修复:

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, settuple. 这些类型中的元素可以拥有自己的类型。

为了标注这些包含内置元素的类型,可以使用 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 变量。

list 中包含着 str 类型的子元素,所以使用 list[str] 声明。

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

typing 导入 List, 首字母 L 大写,代表 python 中的 list 类型。 并且 list 中包含着 str 类型的子元素,所以使用 List[str] 声明。

from typing import List
def process_items(items: List[str]):
    for item in items:
        print(item)

info

这些方括号中的类型被称为“类型参数”。 在本例子中,str 是传递给 List(或者 list 在 python 3.9+) 的类型参数。

意思是:“在 List(或者 list 在 python 3.9+) 中的每一个变量元素的类型都是 str”

tip

如果你是 python 3.9 或更高的版本,不需要从 typing 中导入 List,内置数据结构 list 和 typing 中的 List 是等效的。

通过这样的编写,你的编辑器甚至可以为你提供 list 中的 item 的类型相关提示:

image-20240103210532461

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

2.2.3 Tuple 和 Set

同样去声明 tupleset:

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, 3个元素的类型分别是 intintstr
  • items_s 是一个 set, 其中每个子元素的类型都是 bytes

2.2.4 Dict

要定义一个 dict 类型,需要传递两个类型参数,这两个类型参数需要用逗号分隔开。第一个类型参数是定义的 dictkey 的类型,第二个类型参数是定义的 dictvalue 的类型。

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
  • prices 中的 key 类型全部是 str
  • prices 中的 value 类型全部是 float

2.2.5 Union

union 可以声明一个变量的类型是几种类型中的某一个,例如,可以是 intstr

在 python 3.6 或更加新的版本中(包括 python 3.10),你可以使用 typing 中的 Union ,把一个变量可能接收的类型全部放入一个方括号中去声明。

在 python 3.10 或更加新的版本中,有一种可以通过分隔符号 | 把可能接收的类型分隔声明的新语法,用于表示和 Union 同等的效果。

def process_item(item: int | str):
    print(item)    

from typing import Union

def process_item(item: Union[int, str]):
    print(item)

这两个示例中都表示变量 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 进行代替:

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")

2.2.7 Union还是Optional

如果你现在使用的python 版本低于 3.10 ,这是我非常主观的一个建议:

  • 🚨 避免使用 Optional[SomeType]
  • 🔥 使用 Union[SomeType, None] 代替

虽然它两个是等效的并且底层是想通的,但是我推荐 Union[SomeType, None] 代替 Optional[SomeType] 是因为单词 Optional 似乎暗示变量是可选传递的,不过 Optional 确实是表示 变量可以为 None ,但是并不代表变量不是必需传的。

我认为 Union[SomeType, None] 表述的更加地明确。

这只是单词的名称和含义,但是这些单词能够影响你和你的同事对于代码的潜在看法。

我们以这个函数为例子:

from typing import Optional


def say_hi(name: Optional[str]):
    print(f"Hey {name}!")

name 参数被定义为 Optional[str] , 但是 name 不是一个选传的参数,你不可以按照这种方式调用:

say_hi()  # 这会报错的! 😱

name 参数仍然是必需传递的(不是选传的),因为它没有 default 值,不过,name 可以接收 None :

say_hi(name=None)  # 这样可行, None可以通过 🎉

好消息是,一旦你使用了 python 3.10+ 版本,你就不需要担心这个问题了,因为你可以使用 | 来去定义 Union :

def say_hi(name: str | None):
    print(f"Hey {name}!")

这样就不用去担心 OptionalUnion 的单词含义问题了。

2.2.8 泛型类型

这些可以使用方括号的类型被称为泛型类型或者泛型,例如:

你可以使用内置类型作为泛型(配合方括号,定义子类型在里面):

  • list
  • tuple
  • set
  • dict

和 python 3.8 相同的有(从 typing 中导入):

  • Union
  • Optional
  • ... 其他的

在 python 3.10 中,作为 UnionOptional 的替代,可以使用 | , 这更加的简单和清晰。

你可以使用内置类型作为泛型(配合方括号,定义子类型在里面):

  • list
  • tuple
  • set
  • dict

和 python 3.8 相同的有(从 typing 中导入):

  • Union
  • Optional
  • ... 其他的

{ .annotate }

  • List
  • Tuple
  • Set
  • Dict
  • Union
  • Optional
  • ... 其他的

{ .annotate }

2.3 class 作为 类型注解

也可以使用 class 定义的类作为变量的类型注解。

假设你有一个类叫做 Person,有一个 name 属性:

class Person:
    def __init__(self, name: str):
        self.name = name

然后你可以声明一个 Person 类型的变量:

def get_person_name(one_person: Person):
    return one_person.name

可以发现编辑器依然可以支持提示:image-20240103222431620

注意:这表示 one_person 是 类 Person 的一个实例。

3. Pydantic 模型

Pydantic 是一个用于数据验证的库。

使用 Pydantic,需要通过声明一个 class 类和类属性(每个类属性都有自己的类型)来表示数据的 "形状"。然后你通过传参的形式实例化这个 class 类,它会自动校验传参的数据是否符合类属性的定义类型,并将传入数据转为合适类型,最后还提供一个包含所有数据的object。