我们为什么要使用类型注解
python3.6版本及之后,加入了类型注解的功能。其实以前自己也不是很重视这个事情,不过最近我开始思考关于代码规范和效率的问题,类型注解确实能够很大程度上方便我梳理代码思路,所以,我建议所有人都使用类型注解,这真的是个十分强大又有效的功能。
当然,写这篇BLOG不是为了简单介绍类别注解的语法,这个十分简单了,本次的目的是想记录一下自己对于如何利用更加复杂类型注解的思考。
“注解 ”的本质是什么
先回顾一下常用的类型注解,最常见的类型注解包括整数、列表、布尔类型等等。借用菜鸟教程中的代码:
name: str = "Alice" # 注解为字符串 (str)
age: int = 30 # 注解为整数 (int)
is_student: bool = False # 注解为布尔值 (bool)
scores: list = [95, 88, 91] # 注解为列表 (list)这几种都是常见的类型注解。
当我们需要对更加复杂的对象进行注解的时候,可以采用Python的typing模块。依然是借用菜鸟教程的代码:
from typing import List, Dict, Tuple, Set
from typing import Optional # 当值可能是某种类型或者是 None 时使用
from typing import Union # 当值可能是多种类型之一时使用
# List[int] 表示这是一个只包含整数的列表
numbers: List[int] = [1, 2, 3, 4, 5]
# Dict[str, int] 表示这是一个键为字符串、值为整数的字典
student_scores: Dict[str, int] = {"Alice": 95, "Bob": 88}
# Tuple[int, str, bool] 表示这是一个包含整数、字符串、布尔值的元组
person_info: Tuple[int, str, bool] = (25, "Alice", True)
# Set[str] 表示这是一个只包含字符串的集合
unique_names: Set[str] = {"Alice", "Bob", "Charlie"}
def find_student(name: str) -> Optional[str]:
"""根据名字查找学生,可能找到也可能返回None"""
students = {"Alice": "A001", "Bob": "B002"}
return students.get(name) # 可能返回字符串或None
# 等价于 Union[str, None]
def process_input(data: Union[str, int, List[int]]) -> None:
"""处理可能是字符串、整数或整数列表的输入"""
if isinstance(data, str):
print(f"字符串: {data}")
elif isinstance(data, int):
print(f"整数: {data}")
elif isinstance(data, list):
print(f"列表: {data}")
process_input("hello") # 输出:字符串: hello
process_input(42) # 输出:整数: 42
process_input([1, 2, 3]) # 输出:列表: [1, 2, 3]以上是类型注解常见的用法,但是仔细思考,如果我们想要更丰富去表达一些东西,只用这些基础的方法实在是太受限了。
而想要更加灵活的实现注解,我们就要明白什么情况下编译器判断注解是否合法的根本是什么。
看以下这个代码,思考编译器会怎么看待这个代码。
abc = 100
a: abc = 1
print(a)答案是Python编译可以通过的,只是IDE会提示类型表达式中不允许使用变量。
a: "这个是一个整数" = 1
print(a)
a: 1!=2 = 1
print(a)
也可以通过编译,并且也只是IDE会提示。
所以从根本上,Python解释器并不会管你前面这部分内容究竟是什么,换言之,只要这个表达式是合法的,它究竟是变量,是类型,还是哪怕就是一个字符串,Python解释器不会在意这些。
可以用类似这个方式描述:变量名 : 注解表达式 = 值表达式
提供更多信息
既然知道这种情况以后,我们的注释方式就可以更加自由更加灵活。虽然从底层上来说,Python可以接受任何一种符合规则的表达式,但是,我们的IDE并不会同意我们进行如此随意的写法。实际上,IDE是根据Python所带有的typing规则进行合理性检查的,具体的规则讲解未来或许会进一步说明,在此不详细讲解。我更想分享一下如何进行复杂项目的注释。
想象一下这个场景,你在进行一个深度学习的项目,传入的维度可能是十分复杂的,输出也有可能会存在多维度的输出。
类型别名
我们可以给任意一个复杂的输出起一个类型别名。
from typing import TypeAlias
from torch import Tensor
DetectionResult: TypeAlias = tuple[
Tensor, # boxes [N, 4]
Tensor, # scores [N]
Tensor, # labels [N]
]
def detect(image: Tensor) -> DetectionResult:
...如此代码的可读性会提升很多。
与注释协同
def smpl_forward(
betas: Tensor, # [B, 10]
body_pose: Tensor, # [B, 23, 3]
global_orient: Tensor, # [B, 3]
translation: Tensor, # [B, 3]
) -> tuple[
Tensor, # vertices [B, 6890, 3]
Tensor, # joints [B, 24, 3]
]:
...直接与注释一同使用,能提供更加充分的信息,而且还简单易懂。
当然还有更多的工具能够使用,比如Annotated等,在此不一一赘述了,若有兴趣可以自行详细了解。