本文中主要会介绍Python3中的类型注解的基础概念以及日常使用的技巧。本文主要面对的人群为有着"想对代码实施规范化管理以及增加可读性的”的人们。笔者也会将自己的理解在文中进行阐述,这也算是在和大家交流心得的一个过程。若文中有错误的理解和概念,请大家及时纠正;吸纳大家的建议,对于我来说也是很重要的学习过程之一。
1. 概述
学习和编写过C++和Java的工程师应该都熟悉在声明变量或方法时,需要指定变量或方法的返回值的类型,即显式声明变量。而在Python中,声明变量时无需指定其类型、定义方法时需声明其返回值类型。
Tips: Python的动态类型、一切即为对象概念的体现。
虽然这些操作给Python的编写带来的极大的便利,使得人们可以更加注重与相关逻辑的实现;但同时也会降低代码的可维护性与牺牲一部分代码的可读性。人们常常会为方法增加大量的方法注释和变量注释;同时变量类型错误的异常只会在调用该变量或方法的时候才会暴露出来,往往还需要人为的在调用前通过添加类型检查的方式来处理这类错误。在Python3中引入了类型注解的概念,该功能可以帮助实现类似于C++和Java相同的变量与方法返回值的类型声明。通过为代码段添加类型注解,可以提高代码段的可读性。同时,IDE如果支持解析类型注解,则IDE就能够为Python提供类型检查的功能,这使得可以在编程期间就可以发现和处理变量类型错误的异常。虽然目前Python在运行时还未强制对类型注解进行检查,但笔者认为后续有可能推出强制进行类型检查的功能。因此学习和使用Python3中的类型注解还是比较重要的,推荐大家可以仔细的阅读一下官方文档。
2.语法
2.1 标准语法
在编写新代码时最常用的写法,例如:
def greeting(name: str) -> str:
return 'Hello ' + name
2.2 注释语法
该写法比较适合于在老项目上使用,即通过将类型注解写在注释里的方式来实现。例如:
path = None # type: list[str]
2.3 使用单独文件.pyi来编写类型注解
可以在源代码相同的目录下新建一个与 .py 同名的 .pyi 文件,IDE 同样能够自动做类型检查。该方式的优点为对原来的代码不做任何改动,完全解耦;缺点是相当于要同时维护两份代码。
Tips: 很多大型开源项目都使用该方法进行类型注解的编写。
在编写时,除了使用…来代替函数或对象主体内容以外,其他编写方式都与正常编写代码的语法是相同的。例如:
class WebDriver(RemoteWebDriver): service: Any
def __init__( self, executable_path: str = ..., port: int = ..., options: Any | None = ..., service_args: Any | None = ..., desired_capabilities: Any | None = ..., service_log_path: Any | None = ..., chrome_options: Any | None = ..., keep_alive: bool = ..., ) -> None: ...
def launch_app(self, id): ...
def get_network_conditions(self): ...
def set_network_conditions(self, **network_conditions) -> None: ...
def execute_cdp_cmd(self, cmd, cmd_args): ...
def quit(self) -> None: ...
def create_options(self): ...
3. 注解规范
本章节内会介绍一下常用类型的注解方式。
3.1 自定义类的注解
若变量或方法返回值为自定义的类对象,则直接使用自定义类名进行注解即可。
3.2 容器类型注解
需要从 typing 模块中导入对应的容器类型注解:
from typing import List, Tuple, Dict
但PEP 585后就可以直接使用Python 的内置类型来注解,例如:
list[int] = [1, 2, 3]
tuple[str, ...] = ("a", "b")
dict[str, int] = { "a": 1,"b": 2}
3.3 注解别名
若注解过长,可以通过给类型注解起别名来解决,例如:
Config = list[tuple[str, int], dict[str, str]]
def start_server(config: Config) -> None:
3.4 可变参数
注解方式例如:
def foo(*args: str, **kwargs: int) -> None:
3.5 泛型注解
泛型注解主要用于解决对象的类型信息不能以静态方式写明的情况,即变量类型会随着业务状态而变化,并且无法确定其类型的变化范围。例如,不能确定list或者dict中都包含哪些类型的变量,这时候就可以使用泛型来解决:
from typing import TypeVar
T = TypeVar('T') # T代表任意类型
S = TypeVar('S', int, str) # S代表是int或者是string类型
def first(l: Sequence[T]) -> T: # Generic function return l[0]
3.6 指定类型(常用)
如果能够确定变量的类型范围,那么可以使用 Union 来进行注解。
from typing import Union
T = Union[str, bytes] # 表示只能是str或者bytes
再如,如果一个参数支持使用多种类型,则可以使用Union来注释:
record: Union[ str, Iterable['str'], Point, Iterable['Point'], dict, Iterable['dict'], bytes, Iterable['bytes'], Observable, NamedTuple, Iterable['NamedTuple'], 'dataclass', Iterable['dataclass'] ] = None
3.7 Optional
例如:
Optional[X] = Union[X, None] # 表示被标注的参数要么为 X 类型,要么为 None
3.8 Any(慎用)
一般用于当不知道如何编写类型注解但又不必须使用类型注解式写法的时候使用。
from typing import Any
def greeting(name: Any) -> Any: return "Hello " + name
Any的使用要有克制性,因为其本质和不使用类型注解语法是相同的;只是为了迎合整篇代码的类型注解风格。往往Any都是使用在对相关的代码逻辑不太清楚的位置。可以使用Any来作为临时注解,但后续读懂相关逻辑后,应将Any更改为具体的类型注解。
3.9 可调用对象
因为python支持函数式编程,因此有些方法的形参可能为函数;Callable类型注解就是用描述这种类型的变量。例如:
def f(fn: Callable[[int], str], i: int) -> str:
'''Callable[[int], str]表示该方法的形参或构造方法的形参必须是一个int类型变量,该方法的返回值是string类型变量 '''
return fn(i)
3.10 自引用
该注解类型常用于类似于递归调用方式的方法。由于在进行第一次递归调用的时候,需要调用的方法还没有生成,因此不能使用其他的静态注解方式直接写明,而是需要使用字符串形式来注解。例如:
class Tree(object):
def __init__(self, left: "Tree" = None, right: "Tree" = None):
self.left = left self.right = right
3.11 鸭子类型
Type Hints提供了Protocol来对鸭子类型进行支持,定义类时只需要继承Protocol就可以声明一个接口类型:
from typing import Protocol
当遇到接口类型的注解时,只要接收到的对象实现了接口类型的所有方法,即可通过类型注解的检查。
3.12 返回多个变量(常用)
使用typing.Tuple类型来注解返回多个值的函数的返回值。因为函数返回多个值的本质就是Python将多个值组成一个tuple返回,因此使用typing模块中的Tuple类型来注解。
from typing import Any, Tuple
def multiple_bind(self, ec2_instances: list[str], ip_label: str) -> Tuple[list[str], dict[str, str]]:
pass