Python基础:类型注解(type hint)

1、什么是 Python 类型注解?

1.1 Python动态类型的优缺点

使用静态类型的编程语言,例如 C/C++、Java,需要预先声明函数的变量、参数和返回值的类型。编译器在编译和运行之前检查代码的类型定义是否符合要求。运行时不能随意改变类型,指针变量可显式地用cast()来改变类型。

而 Python 使用动态类型,函数的变量、参数和返回值可以是任何类型。此外,在程序运行时,允许不加申明更改变量类型。Python 解释器在运行时,根据上下文来动态推断变量类型。如下面这个add()函数,输入参数,可以是任意类型。

def add(x,y)
	return x+y
print(add(10+20))
print(add(3.14+5.10))
print(add(10+20.33))
print(add("hello","world"))

如果换成C++, 虽然可用函数模板来实现,显然不如python 简洁与灵活。

template<typename T>  
T add(T x, T y) {  
    return x + y;  
}
// 如果输入参数为整数
int a = 3;  
int b = 4;  
int result = add(a, b);  // result 等于 7
//如果输入参数为浮点数
double c = 3.14;  
double d = 2.71;  
double result = add(c, d);  // result 等于 5.85
//如果x, y 类型不同,必须先转换成同类型,才能调用。
//如果是字符串,必须重写方法

换成 java 的泛型来实现,可读性还不如C++ 。

public class AddFunction {  
    public static <T extends Number> T add(T x, T y) {  
        return (T) x.doubleValue() + y.doubleValue();  
    }  
  
    public static void main(String[] args) {  
        Integer a = 3;  
        Integer b = 4;  
        int resultInt = add(a, b);  // resultInt 等于 7  
  
        Double c = 3.14;  
        Double d = 2.71;  
        double resultDouble = add(c, d);  // resultDouble 等于 5.85  
    }  
}

从上面例子可以看出,动态类型使编程变得容易。但也有代价,因为太灵活,在项目中实现出现因理解不一致,而导致不期望的数据传入,容易造成错误。

Python3 引入类型注解功能

Python3.5 引入了类型注解,英文为type hint, 为您提供了可选的静态类型,可以同时利用静态和动态类型二者优点。 语法上有些类似于 typescript 的类型注解,python 的类型注解使用更加方便,强烈建议在项目开发中应用此功能, 可以帮助规避很多代码中的变量使用错误。

下面用常规方式,定义一个简单的函数,该函数接受一个字符串并返回另一个字符串:

def say_hi(name):
    return f'Hi {name}'


greeting = say_hi('John')
print(greeting)

给函数参数、返回值添加类型注解的语法为:

parameter: type
-> type

例如,下面演示如何对函数的参数和返回值使用类型注解:

def say_hi(name: str) -> str:
    return f'Hi {name}'


greeting = say_hi('John')
print((greeting)

输出:

Hi John

在此新语法中,name参数的类型为:str.

并且 -> str 表示函数的返回值也是str

除了int, str 类型之外,还可以使用其他内置类型,例如strintfloatboolbytes等。

需要注意的是,Python 解释器完全忽略了类型注解。如果将数字传递给函数,程序将运行,而不会出现任何警告或错误:say_hi()

def say_hi(name: str) -> str:
    return f'Hi {name}'

greeting = say_hi(123)
print(greeting)

输出:

Hi 123

若要检查类型注解语法是否符合要求,需要使用静态类型检查器工具。

2、Python类型注解检查器工具:mypy

安装mypy

Python 没有官方的静态类型检查器工具。目前,最流行的第三方工具是 Mypy。有了这个工具,python就可以提前检查到代码中的类型使用错误了,是不是有点像静态语言了。参考 javascript --> typescript的发展轨迹,个人认为,python应该重视类型检查工具的作用。

使用以下命令进行安装:

pip instal mypyCode 

使用mypy

安装后,您可以使用它来在运行程序之前使用以下命令检查类型:mypy

mypy app.py

它将显示以下消息:

app.py:5: error: Argument 1 to "say_hi" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)

该错误指示 的参数是 ,而预期类型是 say_hi``int``str

如果将参数改回字符串并再次运行,它将显示一条成功消息:mypy

Success: no issues found in 1 source file

3、通过类型注解使变量类型保持一致

定义变量时,可以添加类型注解:

name: str = 'John'

变量的类型是str。如果将非字符串的值分配给变量,静态类型检查器将发出错误。例如

name: str = 'Hello'
name = 100

mypy检查后报错,将int类型值赋给了str变量:

app.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str")
Found 1 error in 1 file (checked 1 source file)

向变量添加类型注解不是必须的,因为静态类型检查器通常可以根据分配给变量的值来推断类型。

4、联合类型注解

除了前面提到的,可使用int, str, bool, float 等进行类型注解。 如果允许1个变量接受2种类型以上的输入值,如何实现?

前面提到的加法函数,add() 允许输入参数为任意类型

def add(x, y):
    return x + y

但你想让输入参数可接受 整数,或浮点数 两种输入类型。可以使用typing模块的 Union 类提供多类型注解。

首先,从typing模块导入:Union

from typing import Union

其次,使用Union()方法 创建包含int 和 float 的联合类型:Union[int, float]

def add(x: Union[int, float], y: Union[int, float]) -> Union[int, float]:
    return x + y

以下是完整的源代码:

from typing import Union

def add(x: Union[int, float], y: Union[int, float]) -> Union[int, float]:
    return x + y

从 Python 3.10 开始,您可以使用 X | Y 用于创建联合类型,例如:

def add(x: int | float, y: int | float) -> int | float:
    return x + y

5、类型别名

Python 允许您为类型分配别名,并将别名用于类型注解。例如:

from typing import Union

number = Union[int, float]

def add(x: number, y: number) -> number:
    return x + y

在此示例中,我们为Union[int, float] 分配一个别名number,并在 add()函数中使用该别名。

6、简单集合类型的类型注解

虽然可将变量直接标注为 list, tuple,set,如果希望进一步指定集合中的元素类型,需要使用Typing 模块的 LIst, Tuple, Set,Dict, Sequence等封装类用于注解。

Typing 类型名Python内置类型
Listlist
Tupletuple
Dictdict
Setset
Sequence用于表示 list, tuple 类型
Mapping用于表示字典,set 类型
ByteStringbytes, bytearray, 以及 memoryview 等二进制类型.

注意 typing 模块类型首字母为大写。

from typing import List 
ratings: List[int] = [1, 2, 3]
data: Sequence = [1,2,3]   # 用sequence 来代替 List, Tuple. 

7、复合集合类型的类型注解

如果集合类型的元素也是集合类型,如 [(‘Jack’, 100), (‘Steve’, 300), …] , 列表元素为 tuple,

data_a: List[Tuple[str, int]] = [("Bob", 1), ("Jim", 2), ("Steven", 53)]

再看1个复杂点的类型,

data_b: List[Tuple[Tuple[int, int], str]] = [
    ((10, 20), "red"),
    ((40, 30), "green"),
    ((32, 45), "yellow")
]

显然,不太容易理解, 这类情形下,可通过type alias 类型别名 来注解, 增加可读性

Position = Tuple[int, int]   
# type Position = Tuple[int, int]    # 在V3.12, 前面加type   
Pixel = Tuple[Position, str]
data_b: List[Pixel] = [
    ((10, 20), "red"),
    ((40, 30), "green"),
    ((32, 45), "yellow")
]

8、无类型

如果函数未显式返回值,则可以使用 None 键入 hint 返回值。例如:

def log(message: str) -> None:
    print(message)

9、带类型注解的 FastAPI 例子

FastAPI 应该是最快的 Python Web 开发框架了,其原因除了采用异步执行方式,类型注解也是1个提升速度的因素。
FastAPI 除了要求使用type hint外,比Flask更简洁。速度上要快3-6倍。 下面是1个简单的FastAPI 例子:

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []


@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Any:
    return item


@app.get("/items/", response_model=list[Item])
async def read_items() -> Any:
    return [
        {"name": "Portal Gun", "price": 42.0},
        {"name": "Plumbus", "price": 32.0},
    ]

10、总结

  • 使用类型注解和静态类型检查器工具使代码更加可靠,
  • 对于集合类型的注解,可以使用标准库 typing 模块的相应封闭类型,以及使用类型别名来提升可读性。
  • 使用 mypy工具来帮助查检查代码类型注解错误。