最近看到有人在某乎上吐槽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
和Supertype
Structural 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
为 逆变算子Contravariance
SubType <: 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