Python类型注解(Type Hint)

一. 类型注解介绍

众所周知,Python是一门动态类型语言。与C、C++等静态类型语言不同,在Python中,定义变量不需要声明类型,一个变量上一秒是整型,下一秒就可以是字符串。

这种灵活的特性可以给我们带来极大的便利,但也有一些缺点:我们没法一眼看出这个变量是什么类型、这个函数的返回值是什么类型、应该传什么类型的参数等等,必要时还需要自己进入函数去研究它的源代码。由类型引发的错误可能会随着代码量的上升而增加。💩

1
2
3
4
def divide(num):
return num // 2

divide('10') # 你试试会不会报错:)

为了解决这个问题,Python从3.5版本逐渐引入了类型注解的特性。它可以让你在写Python代码时可选地为变量、参数、返回值标注类型。

类型注解的优势:

  • 可以提高代码的可读性和可维护性;
  • 在运行前就能通过静态类型检查器检测出类型导致的错误;
  • 让代码编辑器提供更全面的智能补全功能。偷懒这一块

二. 基础类型

1
2
3
4
5
6
7
8
9
# 变量
name: str = "张三"
age: int = 5
price: float = 19.99
is_active: bool = True

# 函数参数和返回值
def greet(name: str) -> str:
return f"Hello, {name}"

三. 容器类型

注:typing模块自Python 3.5引入,用于支持类型注解,包含了丰富的类型注解工具。

1. 列表

1
2
3
4
5
6
from typing import List

s: List[int] = [4, 3, 7, 2]

def get_average(scores: List[int]) -> float:
return sum(scores) / len(scores)

2. 元组

1
2
3
4
5
6
7
from typing import Tuple

p: Tuple[int, int] = (300, 400)

def cal_dis(point: Tuple[int, int]) -> float:
x, y = point
return (x**2 + y**2) ** 0.5

3. 字典

1
2
3
4
5
6
7
8
9
from typing import Dict

g: Dict[str, int] = {
"语文": 90,
"数学": 85
}

def cal_average(grades: Dict[str, int]) -> float:
return sum(grades.values()) / len(grades)

4. 集合

1
2
3
4
5
6
from typing import Set

s: Set[int] = {1, 2, 3}

def find_max(ids: Set[int]) -> int:
return max(ids)

可以直接使用内置容器类型进行注解(但内置容器类型后直接加方括号的语法直到Python 3.9后才支持),如:

1
2
def get_average(scores: list[int]) -> float:
return sum(scores) / len(scores)

四. 进阶类型

1. 多种类型

Union表示一个值可以是多种类型中的一种。

1
2
3
4
5
6
from typing import Union

def f(x: Union[int, None]) -> int:
if x is None:
return 0
return x

自Python 3.10版本起可以使用|语法代替:

1
2
3
4
def f(x: int | None) -> int:
if x is None:
return 0
return x

2. 可选类型

Optional[T]等价于Union[T, None],表示一个值可以是类型T或者None

1
2
3
4
5
6
from typing import Optional

def f(x: Optional[int]) -> int:
if x is None:
return 0
return x

3. 任意类型

Any表示可以是任意类型。

1
2
3
4
from typing import Any

def f(x: Any) -> None:
return x

避免过度使用Any,尽可能使用具体的类型来注解。

4. 无返回值类型

NoReturn表示不会正常返回的函数。

1
2
3
4
5
6
7
8
9
10
11
12
from typing import NoReturn
import sys

def error() -> NoReturn:
raise ValueError

def exit() -> NoReturn:
sys.exit(0)

def forever() -> NoReturn:
while True:
pass

5. 可调用类型

Callable表示可以被调用的类型。

高阶函数:

1
2
3
4
5
6
7
from typing import Callable

def operate(func: Callable[[int, int], int], x: int, y: int) -> int:
return func(x, y)

def power(a: int, b: int) -> int:
return a ** b

Callable可以做进一步的定义,即这个可调用对象的参数类型和返回值类型。

第一个是参数类型,需要使用方括号包裹,第二个是返回值类型。

装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
from typing import Callable

def dec(f: Callable[[int, int], int]):
def wrapper(a: int, b: int) -> int:
print(f'args = {a}, {b}')
ret = f(a, b)
print(f'result = {ret}')
return ret
return wrapper

@dec
def add(a: int, b: int) -> int:
return a + b

6. 字面量类型

Literal表示必须是某个具体的值,而不仅仅是某个类型。

1
2
3
4
from typing import Literal

def set_status(status: Literal["success", "error", "pending"]) -> None:
print(f"Status: {status}")

对于:

1
2
3
4
5
6
7
8
9
10
from typing import Literal

class Person:
def __init__(self, name: str, gender: Literal["male", "female"]):
self.name = name
self.gender = gender

g = "female"
a = Person("Bob", g)
b = Person("Bob", "male")

报错:

1
error: Argument 2 to "Person" has incompatible type "str"; expected "Literal['male', 'female']"  [arg-type]

因为Python只知道g是一个字符串,而不知道它是具体的"female"这个值。

此时需要注解g

1
g: Literal["male", "female"] = "female"

7. 泛型

泛型表示可重用的类型模板:

1
2
3
4
5
6
7
8
9
10
from typing import TypeVar

T = TypeVar('T')

def add_two_items(a: T, b: T) -> T:
return a + b

print(add_two_items(1, 1))
print(add_two_items('a', 'b'))
print(add_two_items('a', 1))

T表示任意类型,但是函数参数ab和返回值类型必须都是这个类型。

可以进一步约束类型,限制T是整数、浮点数或者字符串:

1
T = TypeVar('T', int, float, str)

Python 3.12提供了简化语法:

1
2
3
4
5
6
def add_two_items[T: (int, float, str)](a: T, b: T) -> T:
return a + b

print(add_two_items(1, 1))
print(add_two_items('a', 'b'))
print(add_two_items(0.9, 1.9))

五. 类型别名

类型注解本身也是一个对象,因此我们可以为复杂类型创建别名来提高可读性,例如:

1
2
3
4
5
6
7
from typing import Tuple

PointType = Tuple[int, int]

def cal_dis(loc: PointType) -> float:
x, y = loc
return (x**2 + y**2) ** 0.5

六. 自定义类型

给类型起别名并不会创建一个新的类型。对于人类而言,别名和原名具有不同的逻辑含义,但在类型检查器眼中,它们本质上是同一个类型。所以如果你不小心混用了这些名称,类型检查器无法检测出这种逻辑上的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UserId = int
OrderId = int

def delete_user(id: UserId):
print(f"删除用户 {id}")

def delete_order(id: OrderId):
print(f"删除订单 {id}")

user_id: UserId = 1
order_id: OrderId = 123

delete_user(order_id) # 错写
delete_order(user_id) # 错写

虽然写错了,但由于order_idUserIdint被视为同一个东西,类型检查器无法检测出这个漏洞。要解决这个问题,必须创建真正的自定义类型,将intUserIdOrderId三者区分开来。

NewType可以创建真正的自定义类型,语法为:新类型名称 = NewType('新类型名称', 自定义类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from typing import NewType

UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)

def delete_user(id: UserId):
print(f"删除用户 {id}")

def delete_order(id: OrderId):
print(f"删除订单 {id}")

user_id = UserId(1)
order_id = OrderId(123)

delete_user(order_id)
delete_order(user_id)

现在静态类型检查器可以检测出问题:

1
2
test.py:15: error: Argument 1 to "delete_user" has incompatible type "OrderId"; expected "UserId"  [arg-type]
test.py:16: error: Argument 1 to "delete_order" has incompatible type "UserId"; expected "OrderId" [arg-type]

要使用NewType创建的新类型,需要显式地将值转换成这个新类型,否则会被视为两个不一样的类型。

在上面的例子中,必须用UserId(1)OrderId(123)1123转化成UserIdOrderId,因为intUserIdOrderId三者均被视为不同的类型。

如果写成:

1
2
user_id = 1
order_id = 123

就会爆炸

1
2
test.py:15: error: Argument 1 to "delete_user" has incompatible type "int"; expected "UserId"  [arg-type]
test.py:16: error: Argument 1 to "delete_order" has incompatible type "int"; expected "OrderId" [arg-type]

七. 静态类型检查器

静态类型检查器是一种在代码运行前进行类型分析的工具。

mypy是一个流行的静态类型检查工具。

安装:

1
pip install mypy

运行类型检查:

1
mypy *.py

VS Code内置了Pyright作为其类型检查工具(需要安装Pylance扩展),开启方式为:

  1. 点击右下角状态栏花括号,点击“选择类型检查模式”:

    VS Code1

  2. 选择模式:

    VS Code2