前言
众所周知,Python是一门动态类型的语言,它不需要你指定变量的类型,可以实现自动的类型判断,这方便了学习者和代码编写者,但是当项目越来越庞大的时候,很容易就会忘记之前编写的变量是什么类型,这可能会导致严重的错误。
另外,我们在日常的编程中其实也经常遇到,变量嵌套使用多了之后发现IDE已经无法给出方法补全提示,这是因为Python解释器已经无法得知变量的初始类型,默认将类型定义为Any
。加上类型注释后,就能正确得到相应的方法补全提示。
什么是typing?
简单来说就是Python官方用来加强静态类型检查的一个库,有很多好处,本身更多是为了在大型项目规范数据的类型,以方便开发。
- 可以运行前提前发现编写代码时出现的错误,通过
Pylance
和Mypy
等静态检查器可以检查出错误
- 可以限制用户的输入,悬停可以得到函数的文档提示
- 写代码时可以有提示补全(如果不写变量的类型,默认为
Any
)
1 2 3 4
| def add(a:int,b:int)->int: return a+b
add(1,2)
|
typing 和typing_extensions
typing
是Python3.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
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 3 4 5 6 7 8
| from typing import cast class Student(Person): def get_name(self) -> str: 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: 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())
|
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) get_data("a", "b")
|
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")
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)
|
总结:
- Literal 如果需要限制变量的值并提示,可以使用Literal
- TypeVar 如果需要表示”任意类型”,可以使用TypeVar,相比Union可以确保类型的一致性
- 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
也是一个特殊的类型,他可以用来定义字典的键和值的类型(和数据类很相似)
这边使用了Required
和NotRequired
来表示可选字段与必须字段
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')
|