最近看到有人在某乎上吐槽Python越来越卷,一个动态脚本语言开始用typing做静态类型检查。这个说法很哗众取宠。毕竟现在的Python已经不是十年前的Python,不只用于爬虫、运维和数据处理这些传统“脚本”类开发,也逐渐的在各种互联网软件、中间件和客户端开发中扮演重要角色(这部分开发过去是Java, C#,C++的地盘)。
业务需求增长会迫使架构增长;架构增长必然是模块数增加、交互复杂度变高; 伴随而来的是更长的产品生命周期(开发和维护的时间都会拉长);这些增长都会触发团队膨胀;然而团队增长总是很难匹配业务/技术需求增长,毕竟人力成本会有天花板控制、招募和培训难以一朝一夕解决、市面上可匹配岗位的人员供给有限,以及沟通复杂度提升导致人效下降,这些因素最终都会导致团队质量下滑;一增一降的结果就是产品质量不增反降。
小型企业变为大中型企业要从“人治”变为“法治”。软件规模增长要想稳定住质量,也要从依赖“人员质量”的思想转变到“机制和流程改良”思想。更易于写出严谨接口(从而减少他人误用)、自动化生成高质量文档(知识查询和传承)、静态代码分析(将错误消灭在运行前),和代码可读性提升(提升理解、维护和改进成本)都是“机制和流程改良”的实操。显然有类型系统的静态语言在这些方面有天然优势。这也是在中等以上规模软件开发中,静态语言始终占据优势的原因之一。Python引入typing后,在类型系统方面得到提升。使得喜爱Python的开发者,在选择它进行中等规模软件开发时,又多了一个理由。
Python的typing覆盖了各种类型检查场景,符合一个工具要“简单场景简单上手,复杂场景也能支持”的理念。从我自身感受来说,常用的typing需求(例如:参数类型定义,返回值定义,回调函数定义等)可以被40% typing功能覆盖住;一些较少出现,但使用typing会极大减少错误的场景(例如:生成器,协程方法,泛函等)可以被另外40% typing功能覆盖;最后一些高级场景(如:函数Curry化,协程生成器/迭代器等,可以被最后20% typing覆盖。
本篇文章提要如下:
- 变量的
typing比较简单,本文不再介绍。我们从函数Callable的typing讲起 Python的类是一组变量(类变量和实例变量)和一组 第一个参数是类实例self(或类本身)的函数 构成,所以类的typing与函数的typing并无不同,我们也不单独介绍- 函数之后,我们介绍泛型
typing,也是本文中最难、篇幅最大的部分。讲泛型不得不讲协变(covariance)和逆变(contravarance),讲协变和逆变又不得不先讲Subtype,这些内容都集中在泛型这一节中讲解 - 最后会介绍装饰器(即高级函数变换)的
typing
typing的使用
在讲typing之前,我们先简要说明有了typing以后要怎么用起来,从而得到好处?
静态类型检查
首先可以用mypy通过typing进行类型检查
1$ pip install mypy
2Defaulting to user installation because normal site-packages is not writeable
3Collecting mypy
4 Downloading mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (17.3 MB)
5 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 17.3/17.3 MB 12.1 MB/s eta 0:00:00
6Collecting tomli>=1.1.0
7 Using cached tomli-2.0.1-py3-none-any.whl (12 kB)
8Collecting typing-extensions>=3.10
9 Downloading typing_extensions-4.3.0-py3-none-any.whl (25 kB)
10Collecting mypy-extensions>=0.4.3
11 Using cached mypy_extensions-0.4.3-py2.py3-none-any.whl (4.5 kB)
12Installing collected packages: mypy-extensions, typing-extensions, tomli, mypy
13Successfully installed mypy-0.971 mypy-extensions-0.4.3 tomli-2.0.1 typing-extensions-4.3.0
14
15$ mypy main.py # 假设当前目录有main.py文件
16main.py:32: error: Value of type variable "T" of "func" cannot be "int"
17main.py:33: error: Value of type variable "S" of "func" cannot be "float"
18Found 2 errors in 1 file (checked 1 source file)
mypy会输出所有和typing标注有冲突的代码。类似的工具还有pylint
文档生成
如果使用Visual Studio Code,可以直接安装autoDocstring。这个插件帮助开发者自动生成docstring,并且使用typing来推断参数类型。在任意函数下敲下“"""”后回车,即可得到如下自动生成的文档模板:
1def func(a: int, b: str) -> float:
2 """_summary_
3
4 Args:
5 a (int): _description_
6 b (str): _description_
7
8 Returns:
9 float: _description_
10 """
11 return 1.0
然后使用mkgendocs或者pdoc3将docstring抽成markdown文件用于帮助文档渲染。
Callable
1def func(f):
2 return f(1, "a")
3
4
5print(func(lambda x, y: 1.0)) # 1.0
func函数使用Callable来定义对回调函数f的要求:
1from typing import Callable
2
3
4def func(f: Callable[[int, str], float]) -> float:
5 return callback(1, "a")
如果允许回调函数f接收任何类型的参数,可以使用Callable[[Any, Any], float]来定义(有几个输入参数,就需要有几个Any占位),也可以使用...语法占据输入参数位置(可以代表任意多个输入参数)
1from typing import Callable
2
3
4def func(f: Callable[..., float]) -> float:
5 return callback(1, "a")
None or NoReturn?
如果函数没有return语言,运行时默认返回None值
1def func():
2 pass
3
4
5print(func()) # None
可以使用-> None来定义没有返回值的情况
1def func() -> None:
2 pass
如果函数不应该返回任何值,例如:
1def func():
2 raise NotImplementedError()
则应使用NoReturn
1from typing import NoReturn
2
3
4def func() -> NoReturn:
5 raise NotImplementedError()
Variable Arguments
函数使用可变参数的情况:
1def func(a, *args, **kwargs):
2 pass
3
4
5func(1, "a", "b", c=1.1, d=1.2)
*args总会被解释为Tuple,**kwargs则总会被解释为Dict,因此使用typing时,只需指明Tuple的元素类型和Dict的value类型(key类型总是str),如:
1def func(a: int, *args: str, **kwargs: float):
2 pass
3
4
5func(1, "a", "b", c=1.1, d=1.2)
但是如果*args和**kwargs由多种不同的类型组成,则应该使用Any或者object来做typing定义。
生成器
生成器Generator是一种特殊的函数,最早类似于Iterable返回一个Iterator;后来增加了send,throw和close方法变为双向数据传递,从而可以创建协程,称为Generator-based Coroutine(区别于之后async定义的Native Coroutine)。Generator的typing如下:
1from typing import Generator
2
3
4def gen() -> Generator[int, str, bool]:
5 s = yield 1 # yield type: int
6 print(s)
7 return True # return Type
8
9
10g = gen()
11print(g.send(None)) # yield return is 1
12try:
13 g.send("sss") # sss
14except StopIteration as e:
15 ret, = e.args
16 print(ret) # Generator的返回值通过StopIteration的args第一个元素返回给我们
协程
尽管Python的Coroutine是从Generator演化来的,但是我们不能直接用Coroutine来替换Generator的typing。如果我们查看Coroutine和Generator两个typing类的定义,可以看出一些区别:
1# https://docs.python.org/3/library/typing.html#typing.Generator
2class typing.Generator(Iterator[T_co], Generic[T_co, T_contra, V_co]): ...
3
4# https://docs.python.org/3/library/typing.html#typing.Coroutine
5class typing.Coroutine(Awaitable[V_co], Generic[T_co, T_contra, V_co]): ...
二者都继承于Generic,区别是:Generator实现Iterator约束(yield或者yield from要出现在函数体内);而Coroutine是一个可以被await的函数。Coroutine的typing如下:
1from typing import Callable, Coroutine, Any, Awaitable
2
3
4async def func(a: str) -> str:
5 return f"hello {a}"
6
7
8f_1: Callable[[str], Coroutine[Any, Any, str]] = func # func前面加了async之后,func的返回值变为Coroutine[Any, Any, str]
9f_2: Coroutine[Any, Any, str] = func("coroutine") # 返回一个可以await的Coroutine
10
11f_3: Callable[[str], Awaitable[str]] = func # 这个也成为Async Callable,可以传入另一个协程中调用(使用await调用)
12f_4: Awaitable[str] = func("awaitable") # 事实上,我们可以直接将返回值赋给Awaitable
函数重载
这部分内容引用自Python Type Hints - How to Use @overload。
1from __future__ import annotations
2
3from collections.abc import Sequence
4
5
6def double(input_: int | Sequence[int]) -> int | list[int]:
7 if isinstance(input_, Sequence):
8 return [i * 2 for i in input_]
9 return input_ * 2
10
11
12def print_int(a: int):
13 print(a)
14
15
16print_int(double(1))
这个函数的本意是:输入是int类型,则输出也是int类型;输入是list类型则输出类型也是list。但是代码中的typing无法正确表达约束(输入是int类型,输出是list类型的组合也满足现在的typing)。print_int(double(1))在静态类型检查时,报错Argument 1 to "print_int" has incompatible type "Union[int, List[int]]"; expected "int"。需要使用overload装饰器给静态类型检查更多的帮助:
1from __future__ import annotations
2
3from collections.abc import Sequence
4from typing import overload
5
6
7@overload
8def double(input_: int) -> int:
9 ...
10
11
12@overload
13def double(input_: Sequence[int]) -> list[int]:
14 ...
15
16
17def double(input_: int | Sequence[int]) -> int | list[int]:
18 if isinstance(input_, Sequence):
19 return [i * 2 for i in input_]
20 return input_ * 2
21
22
23def print_int(a: int):
24 print(a)
25
26
27print_int(double(1))
两个占位用的overload函数会被静态类型检查工具收集(mypy),但只有没有overload修饰的double函数会用做真正的实现(import阶段,最后import的符号定义会覆盖之前的符号定义)。如上改造后,mypy不会再报任何错误。
泛型
Python支持某种泛型范式很简单,如下这个函数对于所有支持__add__方法的类型都可以执行:
1def func(a, b):
2 return a + b
3
4
5print(func(1, 2)) # 输出3
6print(func([1, 2], [3])) # 输出 [1, 2, 3]
7print(func("Hello ", "World")) # 输出 Hello World
TypeVar
但如何告诉静态类型检查这是一个泛型函数?这时候就要使用TypeVar方法(类似于C#泛型中type parameter):
1from typing import TypeVar
2
3T = TypeVar('T')
4
5def func(a: T, b: T) -> T:
6 return a + b
7
8
9r = func(1, "a") # 检查无法通过,因为形参a是int类型,b是str类型,不是typing提示的同一种类型
TypeVar还支持设置约束:
- 使用
T作为类型的变量,必须是Sequence的subtype - 使用
S作为类型的变量,必须是int或者list
1from typing import Sequence, TypeVar, List
2
3T = TypeVar("T", bound=Sequence)
4S = TypeVar("S", int, list)
5
6def func(a: T, b: S) -> None:
7 ...
8
9func(1, 1) # 检查无法通过,因为a的类型必须是Sequence的subtype
10func([1, 2, 3], 1.0) # 检查无法通过,因为b的类型必须是int或者list
11func([1, 2, 3], [1, 2])
Generic
定义泛型类则需要使用Generic:
1from typing import TypeVar, Generic, List
2
3
4T= TypeVar("T")
5
6
7class SingleLinkedList(Generic[T]):
8 def __init__(self, value: T, next: "SingleLinkedList") -> None:
9 self.value = value
10 self.next = next
11
12 def __repr__(self) -> str:
13 return f"{self.value} -> {self.next}"
14
15
16def build_linked_list_from_int_list(data: List[int]) -> SingleLinkedList[int]:
17 if not data:
18 return None
19 return SingleLinkedList(data[0], build_linked_list_from_int_list(data[1:]))
20
21
22print(build_linked_list_from_int_list([1, 2]))
Subtype Relationship
我们用符号<:来表示is a subtype of这个含义。当且仅当类型A和类型B满足如下条件时,我们说B <: A,B是A的子类型:
- 所有
B的实例 是所有A的实例 的子集,即:isinstance(B(), A)总是真,例如:int值总是float值; - 所有 接收类型
A作为参数的函数 是所有 接收类型B作为参数的函数 的子集。这个表述略有些绕嘴,但它实际在说Callable[[A], None]是Callable[[B], None]这类函数的一个子集;
以上定义参见PEP 483。
原文档中有一句话**The set of values becomes smaller in the process of subtyping, while the set of functions becomes larger.**很好的解释这个关系,即:在subtyping或者“子类化”的过程中,
- 类的实例集合变小了,
Animal的实例范围肯定大于Dog(Dog继承于Animal)。Animal可以包含Cat,Dog,Tiger等,但Dog不能包含Cat,实例范围缩小了; - 函数集合变大了,所有能够处理
Animal的函数必然也能处理Dog,但是如果我们知道参数是Dog后,就可以给每一个接收Animal的函数再做一个针对Dog的优化函数,显然 可以用Dog做参数的函数 至少是 用Animal做参数的函数 总量的两倍,函数集合变大了;
当大家第一次接触Subtype这个概念时,可能会有一些疑惑(至少我是有的),记录在这里:
疑惑1: 满足第一个条件的类型对,难道不是自动满足第二个条件吗?有一个很好的反例:List[int]是不是List[float]的子类型?显然 所有含有int的列表 是 所有含有float的列表 的子集(我们可以用float列表保存一组int值),这说明第一个条件满足;我们来看第二个条件:接收List[float]参数的函数 是否是 接收List[int]参数的函数 子集,如果答案是“是”,则我们应该可以把List[int]作为参数传入Callable[[List[float]], None],但请看下面这个例子(来自官方PEP 483的例子):
1def append_pi(lst: List[float]) -> None:
2 lst += [3.14]
3
4my_list = [1, 3, 5] # type: List[int]
5
6append_pi(my_list) # 运行阶段可以把List[int]作为参数放入
7
8my_list[-1] << 5 # ... 但是,my_list的使用者以为所有元素都是整数,所以会这么使用,3.14 << 5做位操作将会出错
可见问题 接收List[float]参数的函数 是否是 接收List[int]参数的函数 子集的答案是“否”。List[int]不是List[float]的子类型,满足条件1,但不满足条件2。
疑惑2:Subtype可以理解为继承吗?答案为“否”。Subtype是类型系统中的概念。实现类型系统的方案有两大类Nominal Subtyping和Structural Subtyping:
Nominal Subtyping即通过“继承”方式来定义Subtype和SupertypeStructural Subtyping即是所有动态类型语言(如Python和Javascript)中的Duck type,只要两个类型在结构上有共同点,你就可以将其中一个类型视作另一个类型的子类(看上去像鸭子,那它就是鸭子)
Covariance and Contravariance
本部分内容改编自Covariance, Contravariance, and Invariance — The Ultimate Python Guide
1class ParentClass:
2 ...
3
4
5class ChildClass(ParentClass):
6 ...
7
8
9def handle_parentClass(obj: ParentClass):
10 pass
11
12
13def handle_childClass(obj: ChildClass):
14 pass
上面代码中,ChildClass <: ParentClass。那么handle_parentClass和handle_childClass是什么关系?作为函数,是否handle_childClass <: handle_parentClass关系成立?我们按照<:定义来看一下这个问题:
如果一个表达式能够赋值给一个变量,则说明右侧表达式结果的类型为左侧变量类型的子类型,即满足定义1
1# 通过静态类型检查
2parentClassObj: ParentClass = ChildClass()
3
4# mypy报错:error: Incompatible types in assignment (expression has type "ParentClass", variable has type "ChildClass")
5childClassObj: ChildClass = ParentClass() # 违反定义1:ParentClass()不是ChildClass实例的子集
6
7# mypy报错:Incompatible types in assignment (expression has type "Callable[[ChildClass], Any]", variable has type "Callable[[ParentClass], None]")
8f_handle_parentClass: Callable[[ParentClass], None] = handle_childClass # 违反定义1:handle_childClass不是Callable[[ParentClass], None]
9
10# 通过静态类型检查
11f_handle_childClass: Callable[[ChildClass], None] = handle_parentClass
以上实验说明:handle_childClass <: handle_parentClass不成立,相反handle_parentClass <: f_handle_childClass成立,即Callable[[ParentClass], None]是Callable[[ChildClass], None]子类型。我们把以上这个示例用更形式化、更抽象的方式表达:
ParentClass为SuperType,ChildClass为SubType,Callable[[XX], None]为f(XX)算子SubType <: SuperType和f(SuperType) <: f(SubType)成立,则f为 逆变算子ContravarianceSubType <: SuperType和f(SubType) <: f(SuperType)成立,则f为 协变算子Covariance- 如果以上两个关系都不成立,则
f为Invariance
f可以是:
Type -> Tuple[Type, Tuple, ...],Tuple是一种协变算子,即:Tuple[SubType1, SubType2, ...] <: Tuple[SuperType1, SuperType2, ...],例如:
1# 通过静态类型检查,说明Tuple[SubType1, SubType2, ...] <: Tuple[SuperType1, SuperType2, ...]成立
2tuple_parentClass: Tuple[ParentClass, ParentClass, float] = (ChildClass(), ChildClass(), 1)
Type -> Callable[[Type], None],Callable是逆变算子。更一般的,任何Callable对于作为输入参数的类型来说,是逆变算子。Type -> Callable[..., Type],这种类型用于返回值的Callable是协变算子,例如:
1def get_parent() -> ParentClass:
2 ...
3
4
5def get_child() -> ChildClass:
6 ...
7
8
9# mypy报错:error: Incompatible types in assignment (expression has type "Callable[[], ParentClass]", variable has type "Callable[[], ChildClass]")
10f_get_child: Callable[[], ChildClass] = get_parent
11
12# 通过类型检查,说明 Callable[..., SubType] <: Callable[..., SuperType]成立,是一种协变
13f_get_parent: Callable[[], ParentClass] = get_child
更一般的,任何工厂类或者构造方法(只生成类实例,不改变类实例的方法)对于返回对象的类型来说,都是协变算子。
Type -> List[Type],List也是一种算子,同理Iterable,Sequence,MutableSequence这些容器类型都可以作为算子,他们是协变还是逆变?让我们已经用mypy来确认它们的类型
1list_childClass: List[ChildClass]
2list_parentClass: List[ParentClass]
3
4# mypy报错:error: Incompatible types in assignment (expression has type "List[ParentClass]", variable has type "List[ChildClass]")
5# List[SuperType] <: List[SubType]不成立,所以不是Contravariance
6list_childClass = list_parentClass # List[ChildClass] <- List[ParentClass]
7
8# mypy报错:error: Incompatible types in assignment (expression has type "List[ChildClass]", variable has type "List[ParentClass]")
9# List[SubType] <: List[SuperType]不成立,所以不是Covariance
10list_parentClass = list_childClass # List[ParentClass] <- List[ChildClass]
List即不是协变算子,也不是逆变算子,而是Invariance!Tuple和List都是序列类型,为什么一个是协变而另一个不是?这两个类型的区别是一个是“不可变容器”,另一个是“可变容器”。可变容器意味着可以向容器内增加或者删除元素,也可以修改其中某个元素,这将导致我们会将ParentClass类型添加到ChildClass类型的容器内,造成未来某个时刻的误用(上文中append_pi的例子)。
最后我们用mypy验证一下上面这个结论:可变容器是Invariance,而不可变容器是Covariance。
1# Iterable不可变容器 是协变
2iterable_childClass: Iterable[ChildClass]
3iterable_parentClass: Iterable[ParentClass]
4# mypy error: Incompatible types in assignment (expression has type "Iterable[ParentClass]", variable has type "Iterable[ChildClass]")
5iterable_childClass = iterable_parentClass
6iterable_parentClass = iterable_childClass
7
8# Sequence不可变容器 是协变
9seq_childClass: Sequence[ChildClass]
10seq_parentClass: Sequence[ParentClass]
11# mypy error: Incompatible types in assignment (expression has type "Sequence[ParentClass]", variable has type "Sequence[ChildClass]")
12seq_childClass = seq_parentClass
13seq_parentClass = seq_childClass
14
15# MutableSequence可变容器 不是协变也不是逆变
16mut_seq_childClass: MutableSequence[ChildClass]
17mut_seq_parentClass: MutableSequence[ParentClass]
18# mypy error: Incompatible types in assignment (expression has type "MutableSequence[ParentClass]", variable has type "MutableSequence[ChildClass]")
19mut_seq_childClass = mut_seq_parentClass
20# mypy error: Incompatible types in assignment (expression has type "MutableSequence[ChildClass]", variable has type "MutableSequence[ParentClass]")
21mut_seq_parentClass = mut_seq_childClass
Covariance and Controvariance Typing
现在我们来看一下为什么要在typing中引入协变和逆变。
1from typing import TypeVar, Generic, Iterator
2
3
4T = TypeVar("T")
5
6
7class MyContainer(Generic[T]):
8 def __init__(self):
9 ...
10
11 def __iter__(self) -> Iterator[T]:
12 ...
13
14
15def func(container: MyContainer[ParentClass]):
16 for item in container:
17 print(item)
在上面这段代码中,我们是否可以把MyContainer[ChildClass]()传入func中?代码逻辑上看,100%是没问题的。但是mypy却不这么认为,如果我们检查
1# mypy error: Argument 1 to "func" has incompatible type "MyContainer[ChildClass]"; expected "MyContainer[ParentClass]"
2func(MyContainer[ChildClass]())
该如何调整以上代码,使得mypy能验证通过呢?
MyContainer[T]是一个算子,func是另一个算子,两个算子嵌套形成新算子Type -> Callable[[MyContainer[T]], None]。只有当新算子Callable[[MyContainer[ParentClass]], None] <: Callable[[MyContainer[ChildClass]], None]成立时,即 接收ParentClass为参数的函数 是 接收ChildClass为参数的函数 的子集,即任何接收ParentClass的方法都可以接收ChildClass,才能让func(MyContainer[ChildClass]())通过静态类型检查。以上要求等同于要求Type -> Callable[[MyContainer[T]], None]是逆变算子。已知Callable是逆变算子,逆变算子嵌套协变算子才能产生新逆变算子,因此要求MyContainer[T]是一个协变算子。问题变为:如何让MyContainer[T]是一个协变算子?TypeVar提供了covariant设置:
1T = TypeVar("T") # 默认是invariant
2T_co = TypeVar("T_co", covariant=True)
3T_contra = TypeVar("T_contra", contravariant=True)
4
5
6class CoContainer(Generic[T_co]):
7 def __init__(self):
8 ...
9
10 def __iter__(self) -> Iterator[T_co]:
11 ...
12
13
14class ContraContainer(Generic[T_contra]):
15 def __init__(self):
16 ...
17
18 def __iter__(self) -> Iterator[T_contra]:
19 ...
Generic[T_co]声明可以使CoContainer[T_co]成为和Sequence一样的协变算子,反之ContraContainer[T_contra]是逆变算子。我们测试一下:
1def func_1(container: CoContainer[ParentClass]):
2 for item in container:
3 print(item)
4
5
6def func_2(container: ContraContainer[ParentClass]):
7 for item in container:
8 print(item)
9
10
11# 通过静态类型检查
12func_1(CoContainer[ChildClass]())
13
14# mypy error: Argument 1 to "func_2" has incompatible type "ContraContainer[ChildClass]"; expected "ContraContainer[ParentClass]"
15func_2(ContraContainer[ChildClass]())
ParamSpec和Concatenate
假设我们有一个装饰器如下,该如何用typing约束类型?
1def add_logging(f):
2 async def inner(*args, **kwargs):
3 await log_to_database()
4 return f(*args, **kwargs)
5 return inner
6
7
8@add_logging
9def takes_int_str(x: int, y: str) -> int:
10 return x + 7
使用前面讲过的Callable,应该是:
1R = TypeVar("R")
2
3
4def add_logging(f: Callable[..., R]) -> Callable[..., Awaitable[R]]:
5 async def inner(*args: object, **kwargs: object) -> R:
6 await log_to_database()
7 return f(*args, **kwargs)
8 return inner
9
10
11@add_logging
12def takes_int_str(x: int, y: str) -> int:
13 return x + 7
14
15
16await takes_int_str(1, "A") # 通过静态类型检查
17await takes_int_str("B", 2) # 通过静态类型检查, 运行时报错
takes_int_str满足typing:Callable[..., R]检查;同时takes_int_str变为inner后的参数类型为object,所以无论inner(1, "A")还是inner("B", 2)都满足类型检查。因此两条takes_int_str都会通过静态类型检查,但第二条语句会触发运行时错误。
为了解决以上问题,Python提供ParamSpec类型(见PEP612)。ParamSpec将把参数类型的约束保留并传递给inner函数。inner(x: int, y: str)有了更明确的参数类型要求,inner("B", 2)语句也就无法通过静态分析。
1P = ParamSpec("P")
2R = TypeVar("R")
3
4
5def add_logging(f: Callable[P, R]) -> Callable[P, Awaitable[R]]:
6 async def inner(*args: P.args, **kwargs: P.kwargs) -> R:
7 await log_to_database()
8 return f(*args, **kwargs)
9 return inner
10
11
12@add_logging
13def takes_int_str(x: int, y: str) -> int:
14 return x + 7
15
16
17await takes_int_str(1, "A") # Accepted
18await takes_int_str("B", 2) # Correctly rejected by the type checker
与ParamSpec一起加入typing体系的还有Concatenate,可以很容易的向被修饰的函数中增加新的参数:
1def with_request(f: Callable[Concatenate[Request, P], R]) -> Callable[P, R]:
2 def inner(*args: P.args, **kwargs: P.kwargs) -> R:
3 return f(Request(), *args, **kwargs)
4 return inner
5
6
7@with_request
8def takes_int_str(request: Request, x: int, y: str) -> int:
9 # use request
10 return x + 7
11
12
13takes_int_str(1, "A") # Accepted
14takes_int_str("B", 2) # Correctly rejected by the type checker