前言

众所周知,Python是一门动态类型的语言,它不需要你指定变量的类型,可以实现自动的类型判断,这方便了学习者和代码编写者,但是当项目越来越庞大的时候,很容易就会忘记之前编写的变量是什么类型,这可能会导致严重的错误。

另外,我们在日常的编程中其实也经常遇到,变量嵌套使用多了之后发现IDE已经无法给出方法补全提示,这是因为Python解释器已经无法得知变量的初始类型,默认将类型定义为Any。加上类型注释后,就能正确得到相应的方法补全提示。

什么是typing?

简单来说就是Python官方用来加强静态类型检查的一个库,有很多好处,本身更多是为了在大型项目规范数据的类型,以方便开发。

  • 可以运行前提前发现编写代码时出现的错误,通过PylanceMypy等静态检查器可以检查出错误
  • 可以限制用户的输入,悬停可以得到函数的文档提示
  • 写代码时可以有提示补全(如果不写变量的类型,默认为Any
1
2
3
4
def add(a:int,b:int)->int:
return a+b

add(1,2)

typing 和typing_extensions

typingPython3.5引入的默认库,可以直接导入。为了让其他版本也能使用typing,官方创建了一个第三方库typing_extension来向下兼容

可以使用一下命令安装:

1
pip install typing_extensions -U

VsCode设置类型检查

在Pylance扩展中将 Type Checking Mode 从 off –> basic

在Python扩展中将Language Serve改为Pylance

PyCharm不需要自己设置

快速上手

类型注解写在变量或者函数的后面,变量需要加上:,函数返回值需要加上->

基础的类型分为

  • int
  • float
  • bool
  • str
  • bytes
  • Any
  • Tuple
  • List,
  • Dict

在Python3.9之前,只能通过导入List[int]Dict[int]来定义list和dict类型,Python3.9之后可以直接使用list[int]dict[str,int]来做定义类型(前面是键,后面是值)。

1
2
3
4
5
from typing import Dict

my_dict: Dict[str, int] = {"壹": 1, "贰": 2, "叁": 3}

print(my_dict)

使用类作为注解

如果类都是在一个完整的Python文件中定义,那么直接使用类名即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Teacher:
def get_name(self) -> str:
return "王老师"

def get_teacher_name(self, student) -> str:
return student.get_name()


class Student:
def get_name(self) -> str:
return "张三"

def get_teacher_name(self, teacher: Teacher) -> str:
return teacher.get_name()


s = Student()
t = Teacher()
print(s.get_name()) # 张三
print(s.get_teacher_name(t)) # 王老师

如果Teacher在另一个Python文件中,那么我们就需要使用其他的方法。

如下需要给get_student_name写上类型提示,如果直接导入Student会出现most likely due to a circular import的报错,这时候需要使用TYPE_CHECKING来虚假导入,这样可以在不导入的情况下获得类型的提示。

因为是虚假导入,修改后报错为NameError: name 'Student' is not defined,这时候可以给Student加上’’来消除报错。

最一劳永逸的方法是在开头(一定要在开头)导入from __future__ import annotations,这样就不需要写’’也不会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from __future__ import annotations
from typing import TYPE_CHECKING

# TYPE_CHECKING默认为False
if TYPE_CHECKING:
# 虚假导入
from student import Student

class Teacher:
def get_name(self) -> str:
return "王老师"

def get_student_name(self, student:Student) -> str:
return student.get_name()

final和Final

Final是一个类型,表示值不可被修改(实际上也只是一个警告,修改还是能成功的)

1
2
3
A: Final[int] = 10

A = 11 # 出现警告

final是一个装饰器,用实现类中不可重写的方法,一旦被重写就会出现警告。(这个警告需要将Pylance的检查等级上升到standard)

1
2
3
4
5
6
7
8
9
10
11
12
class Person:
@final
def get_name(self) -> str:
return ""


class Student(Person):
def get_name(self) -> str: # 警告
return "张三"

def get_teacher_name(self, teacher: Teacher) -> str:
return teacher.get_name()

cast强制类型转化

实际上是欺骗类型检查器的。

需要先从typing中导入cast

cast有两个参数

  1. 第一个 - 类型检查器希望得到的类型
  2. 第二个 - 实际返回的类型
1
2
3
4
5
6
7
8
from typing import cast
class Student(Person):
def get_name(self) -> str:
# return None
return cast(str, None)

def get_teacher_name(self, teacher: Teacher) -> str:
return teacher.get_name()

类型注解选项

常用在输入的值可以是多个类型的时候

  • Union[T1,T2] 表示既可以是T1类型,也可以是T2类型
  • Optional[T] 表示既可以是T类型,也可以是None类型(只能写一个参数类型)

如果是在Python3.10的版本,还可以使用T1|T2来表示可以是T1也可以是T2类型

实际上Optional[T]等价于Union[T, None]

1
2
3
4
5
6
7
8
9
from typing import Final, final, cast, Union,Optional

class Student(Person):
def __init__(self) -> None:
super().__init__()
# 三种写法等价
# self._teacher: Optional[Teacher] = None
# self._teacher: Union[Teacher,None] = None
self._teacher: Teacher|None = None

特殊的类型

  • Self - 返回类本身 多用于链式编程
  • ClassVar - 提示class属性值被实例化对象修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from typing import Self, ClassVar


class Number:

def __init__(self, num: int) -> None:
self._num: int = num

def add_one(self) -> Self:
self._num += 1
return self

def multiply_two(self) -> Self:
self._num *= 2
return self

def __str__(self) -> str:
return str(self._num)


n: Number = Number(10)

print(n.add_one().multiply_two().add_one()) # 23

ClassVar后,类的属性值就只能被类自身修改,而不能被实例化修改(实际上还是可以成功修改,只是会有警告)。

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


class Person:
name: ClassVar[str] = "张三"

def __init__(self) -> None:
pass

if __name__ == "__main__":
p: Person = Person()
print(p.name) # 张三
p.name = "李四"
print(p.name) # 李四
print(Person.name) # 张三

Literal字符串补全

相比之前的类型定义只能进行检查,Literal可以在我们输入的时候直接给出预设的类型补全(只能是预设的类型)

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


def get_data(data_type:Literal["json","csv"])->Any:
if data_type == "json":
return {"data":"json data"}
elif data_type == "csv":
return {"data":"csv data"}
else:
raise ValueError("Invalid data type")


get_data("json")

泛型

  • TypeVar 确保前后的类型一致性
  • Generic 实例化的时候才指定对应的类型

TyperVar实际上是为了解决Union前后不一致的问题,如下面的一个例子:

1
2
3
4
5
6
7
from typing import Union

U = Union[int, str]


def get_data(a: U, b: U) -> U:
return a + b

在一开始就已经报错为预期类型为“U”时,类型“int”和“str”不支持运算符“+”。预期类型为“U”时,类型“str”和“int”不支持运算符“+”

这说明前后的类型不是一一对应的,a的类型是int的时候,b的类型可以是int也可以是str。这时候就可以使用TypeVar,他可以实现前后严格的一一对应关系

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

T = TypeVar("T", int, str)


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


get_data(1, 2) # 3
get_data("a", "b") # "ab"

Generic主要用于定义泛型类,泛型类是指类中的属性或方法的类型不是固定的,而是可以根据实例化时传入的类型来确定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from typing import Union, TypeVar, Generic, List

T = TypeVar("T")


# 使用Generic主要用于定义泛型类,
# 泛型类是指类中的属性或方法的类型不是固定的,
# 而是可以根据实例化时传入的类型来确定。
class Mylist(Generic[T]):
def __init__(self, items: List[T]) -> None:
self.items = items

def append(self, item: T) -> None:
self.items.append(item)

def __str__(self) -> str:
return str(self.items)


if __name__ == "__main__":
my_list: Mylist[int] = Mylist([1, 2, 3])
my_list.append(4)
print(my_list)

my_list2: Mylist[str] = Mylist(["a", "b", "c"])
my_list2.append("d")
print(my_list2)

总结:

  1. Literal 如果需要限制变量的值并提示,可以使用Literal
  2. TypeVar 如果需要表示”任意类型”,可以使用TypeVar,相比Union可以确保类型的一致性
  3. Generic 如果需要定义泛型类,可以使用Generic,在定义的时候不直接指定类型,而是使用Generic,使用的时候在进行指定

重载

重载一般都意味着在一定的规则上进行改写。

  • overload 用来重写函数签名
  • override 用来检查子类重写函数名是否则正确

overload装饰器用于声明函数的重载,可以在不改写方法体的情况下,进行签名的重载(也就是提示)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from typing import overload

"""
overload装饰器用于声明函数的重载
如下面的add函数,我们定义了两个函数签名,
一个用于整数相加,一个用于字符串相加
然后再定义一个不带装饰器的add函数,用于实际的实现
"""

@overload
def add(a: int, b: int) -> int:
"""
用于两个整数相加
"""
...


@overload
def add(a: str, b: str) -> str:
"""
用于两个字符串相加
"""
...


def add(a, b):
return a + b


if __name__ == "__main__":
# 两个函数所得到的提示信息是不同的
add("1", "2")
add(1, 2)

override使用来检查子类继承的函数名是否正确的检查,他可以让重写方法变得更加的规范,如下面的make_sound函数,如果子类中写成make_sounds就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from typing_extensions import overload, override


class Animal:
def __init__(self, name: str) -> None:
self.name = name

def make_sound(self) -> str:
return ""


class Dog(Animal):

def __init__(self, name: str) -> None:
super().__init__(name)

@override
def make_sound(self) -> str:
print(self.name)
return "汪汪汪"


class Cat(Animal):
def __init__(self, name: str) -> None:
super().__init__(name)

@override
def make_sound(self) -> str:
return "喵喵喵"


d: Dog = Dog("dog")
print(d.make_sound())

c: Cat = Cat("cat")
print(c.make_sound())

协议

Protocol是一个抽象基类,它的作用是用来定义协议。它的子类可以用来表示协议,在实现的过程中子类的方法必须和父类完全一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from typing_extensions import Protocol

class Animal(Protocol):
def eat(self):
pass

def sleep(self):
pass


class Dog:
def eat(self):
print("Dog eat")

def sleep(self):
print("Dog sleep")


class Cat:
def eat(self):
print("Cat eat")

def sleep(self):
print("Cat sleep")


def animal_do(animal: Animal):
animal.eat()
animal.sleep()

animal_do(Dog())
animal_do(Cat())

TypeDict

TypeDict也是一个特殊的类型,他可以用来定义字典的键和值的类型(和数据类很相似)

这边使用了RequiredNotRequired来表示可选字段与必须字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from typing_extensions import TypedDict
from typing import Required, NotRequired

"""
Required表示必须的字段
NotRequired表示可选的字段
"""

class Student(TypedDict):
name: Required[str]
age: Required[int]
email: NotRequired[str]


my_dict:Student = {"name": "Tom", "age": 18}

print(my_dict["name"])

数据类dataclass的实现方法也回忆一下:

1
2
3
4
5
6
7
8
9
10
11
from dataclasses import dataclass
from typing import Optional

@dataclass
class Teacher:
name:str
age:int
email:Optional[str] = None


teacher:Teacher = Teacher(name="Tom", age=18)

unpack解包

unpack是一个特殊的类型,他可以用来解包一个字典。可以给**kwargs提供更加精确的类型提示,可以提供类型注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from typing_extensions import TypedDict, Unpack
from typing import Required, NotRequired, AnyStr


class Teacher(TypedDict):
name: Required[str]
age: Required[int]
email: NotRequired[str]


def get_information(*args, **kwargs: Unpack[Teacher]) -> str:
return f"Name:{kwargs['name']},Age:{kwargs['age']}"


my_dict: Teacher = {"name": "Tom", "age": 18}
print(get_information(**my_dict))

动态导入

如果写一个项目不太清楚用户使用的是否支持typing库,可以使用import_module进行动态导入

1
2
3
4
5
6
from importlib import import_module

try:
typing = import_module('typing')
except:
typing = import_module('typing_extensions')