在这里插入图片描述

  • 类型提示,对应当前的python 3.12 中 Typing Hint英文词语(官方文档有时也称类型注解(type annotation)。正如 hint 的英文本义,Typing Hint 只是对当前变量类型的提示,并非强制类型申明,
  • 类型提示与类型检查,是 Python3.5 后各版本都非常重视的功能, 👍。Type Hint 对于提升代码质量与可读性非常有帮助,越来越多的库开始支持 Type Hint, 如 FastAPI, Numpy, Pandas, Pytorch 等。

1、什么是 Python 类型注解?

1.1 Python动态类型的优缺点

使用静态类型的编程语言,例如 C/C++、Java,必须预先声明变量、函数参数和返回值类型。编译器在编译和运行之前会强制检查代码的类型定义是否符合要求。运行时不能随意改变类型,当然需要的话,C++指针变量可改变指向对象的类型,但必须显式地用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  
    }  
}

从上面例子可以看出,动态类型的优点:使编程变得容易,代码可读性更佳。这也是Zen of Python(Python之禅)所倡导的:简单比复杂好,复杂比错综复杂好。但也有代价,因为灵活,容易出现因前后理解不一致而造成的错误, 如经常遇到的1个典型问题:传入SQL的数据与数据库期望的不一致而导致SQL操作失败。Java语法最死板,若成功编译后,反而不容易出错。

1.2 Python3 类型注解的基本语法

Python3.5 引入了类型注解(typing 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

1.3 典型类型注解代码示例

# Type hint for a function that takes a list of integers and returns a list of strings
def process_numbers(numbers: List[int]) -> List[str]:
    return [str(num) for num in numbers]

# Type hint for a function that takes a dictionary with string keys and integer values
def calculate_total(data: Dict[str, int]) -> int:
    return sum(data.values())

# Type hint for a function that takes a datetime object and returns a formatted string
def format_date(date: datetime) -> str:
    return date.strftime("%Y-%m-%d")

# Type hint for a function that takes a Union of two types as input
def process_data(data: Union[List[int], List[str]]) -> List[str]:
    if isinstance(data, list):
        return [str(item) for item in data]
    else:
        return []

# Type hint for a function that returns a generator object
def generate_numbers() -> Generator[int, None, None]:
    for i in range(10):
        yield i

# Type hint for a class method that returns an instance of the class itself
class MyClass:
    def __init__(self, value: int):
        self.value = value

    def double_value(self) -> "MyClass":
        return MyClass(self.value * 2)

在这些示例中,可以看到如何使用类型注解来指定预期类型的函数参数和返回值。它们有助于澄清代码的意图,并为静态类型检查工具和IDE类型检查插件提供信息,以提供更好的代码建议并捕获潜在的错误。

2、类型检查工具mypy

mypy 工具是Python代码类型检查工具,用于检查类型注解语法是否符合要求。

安装mypy

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

使用以下命令进行安装:

pip instal mypy

使用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

如果使用 Vscode IDE, 可以将mypy 设置为python代码的静态类型检查器
打开 Vscode 的全局设置文件 settings.json.

{
  "python.linting.mypyEnabled": true,
  "python.linting.mypyArgs": [
    "--follow-imports=silent",
    "--show-column-numbers",
    "--allow-untyped-defs",
    "--allow-subclassing-any",
    "--allow-untyped-calls",
    "--strict"
  ]
} 

开关选项说明

  • –strict 表示严格模式
  • –allow-untyped-defs: 对于未做类型注解的函数,不显示错误
  • –allow-subclassing-any: 允许对Any类型的值进行子类化。一些框架可以使用Mypy抛出这些错误,因此如果您有这些错误,请添加这一行。
  • –allow-untyped-calls: 允许您在代码中调用无类型注解函数

重启Vscode 后,设置即生效。 团队项目可采用此方式,相当于强行要求在python中进行类型检查,帮助提高代码质量。

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

但你想限制参数x,y为整数或浮点数类型,而非单一类型。可以使用typing模块的 Union类注解。

4.1 Union 联合类型注解

首先,从typing模块导入:Union

from typing import 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

4.2 使用 | 符号对联合类型注解

Python3.11 以后,支持 | 注解联合类型, 更加简便

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

4.3 Optional 类型

arg: Optional[int] 相当于 Union[int, None], 即变量可能为None.

from typing import Optional, Mapping

message: Optional[str] = "hello python"
data: Mapping[str,Optional[str]] = {'name': 'Jack', 'hobbies': 'Hiking' }

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 模块类型首字母为大写。

标注 list 类型变量

用typing模板的List 或者Sequence, 注意 list 类型注解,只接收1个类型参数。

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

也可以写成

rating: list[int] = []

标注Tuple元组类型变量

Tuple 类型标注可以接受任意个参数,按元素位置设置类型

# 元素类型为int
x: tuple[int] = (5,)

# 第1个元素为 int, 第2个元素为str 
y: tuple[int, str] = (5, "foo")

标注字典类型变量

使用 typing模板的 Mapping 来标注,Mapping有两个参数,第1个参数为key的类型,第2个参数为value的类型。

from typing import Mapping, Sequence
x: Mapping[str, str | int] = {}   
x['name']=3.113       # 会提示错误

Mapping[str, str | int] 表示key为str类型, value 类型为 str 或者 int .

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、特殊Typing类型

8.1 无类型 None

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

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

8.2 函数对象做参数的类型注解

如果参数为1个函数对象,类型注解的形式如下:
Callable[[int], str]
对应于函数对象的参数与返回值 (int) -> str.

from typing import Callable 

def foo(x: int, callback: Callable[[int],str]) -> str: 
    return callback(x)

9、Generic Type(也称泛型)

如果在代码中采用了Type Hints 规范,但又不想失去调用函数时可输入任意类型参数的便利,可将不确定类型的变量申明为 generic type

9.1 函数定义中使用 generic type

Step1, 先定义1个 type varialbe 类型变量,

使用TypeVar() 方法来定义类型变量 , 主要用法:

T = TypeVar('T')  # 可以是任意类型
S = TypeVar('S', bound=str)  # 必须是 str 类型
A = TypeVar('A', bound=str|bytes)  # 必须是 str 或 bytes

TypeVar() 第1个参数为名字, 可以任意取。类型变量的作用是告知 type checker , 这是1个generic type variable.

from typing import Sequence, Mapping, TypeVar

T = TypeVar('T')       # 将 T 做为类型变量

Step-2, 函数定义时,可将函数参数、函数体内变量、返回值,申明为泛型

def doubleit(n: T) -> T:
    return n*n

Step-3, 输入不同类型参数验证

x: int = 100          
print(doubleit(x))     # 输入int类型

y: float = 33.33       # 输出 float 类型
print(doubleit(y))

output

10000
1110.8889

9.2 定义多个类型变量

有时,函数有多个输入参数,且类型不相同,可定义多个类型变量

X = TypeVar('X')
Y = TypeVar('Y')

def lookup_name(mapping: Mapping[X, Y], key: X, default: Y) -> Y:
    try:
        return mapping[key]
    except KeyError:
        return default

# 测试泛型函数
rows: Mapping[str, str | int] = {
    'product': 'tv',
    'price': 5900,
    'model': 'XT601',
    'size': 75,
    'screen': 'IPS LED',
}

print(lookup_name(rows, 'size', 50))

9.3 在 class类定义中使用 generic type

typing提供了1个Generic 类做为使用generic type 泛型的class 的基类,

class MyClass( **typing.Generic[T]** ):

from typing import TypeVar, Generic

T = TypeVar('T')

class MyClass(Generic[T]):
    data_a: T

    def __init__(self, data: T) -> None:
        self.data_a = data

    def display(self) -> None:
        print(self.data_a)


print("Generic type in class defiition")
ma = MyClass(100)
ma.display()

mb = MyClass("it is a CAT")
mb.display()

10、使用 Typing Hint 的 FastAPI 示例

FastAPI 是最快的 Python Web 开发框架之一,其原因除了采用异步执行方式,类型注解也是1个提升速度的因素。
FastAPI 框架要求使用type hint 类型注解,更容易管理代码质量。 下面是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},
    ]

11、总结

在这里插入图片描述

  • 对变量与函数使用类型注解,可以提高代码质量,
  • 对于容器类型的注解,可以使用标准库 typing 模块的相应容器类型,可以使用类型别名来提升可读性。
  • 使用 mypy工具来帮助查检查代码类型注解错误。
  • 通过generic type 泛型增加函数与类定义的灵活性。
  • 也可以使用 class类, dataclass 做类型注解。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐