Python Typing

Posted on | 8195 words | ~17 mins
Python

最近看到有人在某乎上吐槽Python越来越卷,一个动态脚本语言开始用typing做静态类型检查。这个说法很哗众取宠。毕竟现在的Python已经不是十年前的Python,不只用于爬虫、运维和数据处理这些传统“脚本”类开发,也逐渐的在各种互联网软件、中间件和客户端开发中扮演重要角色(这部分开发过去是Java, C#C++的地盘)。

业务需求增长会迫使架构增长;架构增长必然是模块数增加、交互复杂度变高; 伴随而来的是更长的产品生命周期(开发和维护的时间都会拉长);这些增长都会触发团队膨胀;然而团队增长总是很难匹配业务/技术需求增长,毕竟人力成本会有天花板控制、招募和培训难以一朝一夕解决、市面上可匹配岗位的人员供给有限,以及沟通复杂度提升导致人效下降,这些因素最终都会导致团队质量下滑;一增一降的结果就是产品质量不增反降。

小型企业变为大中型企业要从“人治”变为“法治”。软件规模增长要想稳定住质量,也要从依赖“人员质量”的思想转变到“机制和流程改良”思想。更易于写出严谨接口(从而减少他人误用)、自动化生成高质量文档(知识查询和传承)、静态代码分析(将错误消灭在运行前),和代码可读性提升(提升理解、维护和改进成本)都是“机制和流程改良”的实操。显然有类型系统的静态语言在这些方面有天然优势。这也是在中等以上规模软件开发中,静态语言始终占据优势的原因之一。Python引入typing后,在类型系统方面得到提升。使得喜爱Python的开发者,在选择它进行中等规模软件开发时,又多了一个理由。

Pythontyping覆盖了各种类型检查场景,符合一个工具要“简单场景简单上手,复杂场景也能支持”的理念。从我自身感受来说,常用的typing需求(例如:参数类型定义,返回值定义,回调函数定义等)可以被40% typing功能覆盖住;一些较少出现,但使用typing会极大减少错误的场景(例如:生成器,协程方法,泛函等)可以被另外40% typing功能覆盖;最后一些高级场景(如:函数Curry化,协程生成器/迭代器等,可以被最后20% typing覆盖。

本篇文章提要如下:

  • 变量的typing比较简单,本文不再介绍。我们从函数Callabletyping讲起
  • 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或者pdoc3docstring抽成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的元素类型和Dictvalue类型(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;后来增加了sendthrowclose方法变为双向数据传递,从而可以创建协程,称为Generator-based Coroutine(区别于之后async定义的Native Coroutine)。Generatortyping如下:

 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第一个元素返回给我们

协程

尽管PythonCoroutine是从Generator演化来的,但是我们不能直接用Coroutine来替换Generatortyping。如果我们查看CoroutineGenerator两个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的函数。Coroutinetyping如下:

 1from typing import Callable, Coroutine, AnyAwaitable
 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 <: ABA的子类型:

  1. 所有 B的实例 是所有 A的实例 的子集,即:isinstance(B(), A)总是真,例如:int值总是float值;
  2. 所有 接收类型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的实例范围肯定大于DogDog继承于Animal)。Animal可以包含CatDogTiger等,但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 SubtypingStructural Subtyping

  • Nominal Subtyping即通过“继承”方式来定义SubtypeSupertype
  • Structural Subtyping即是所有动态类型语言(如PythonJavascript)中的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_parentClasshandle_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]子类型。我们把以上这个示例用更形式化、更抽象的方式表达:

  • ParentClassSuperTypeChildClassSubTypeCallable[[XX], None]f(XX)算子
  • SubType <: SuperTypef(SuperType) <: f(SubType)成立,则f逆变算子 Contravariance
  • SubType <: SuperTypef(SubType) <: f(SuperType)成立,则f协变算子 Covariance
  • 如果以上两个关系都不成立,则fInvariance

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也是一种算子,同理IterableSequenceMutableSequence这些容器类型都可以作为算子,他们是协变还是逆变?让我们已经用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即不是协变算子,也不是逆变算子,而是InvarianceTupleList都是序列类型,为什么一个是协变而另一个不是?这两个类型的区别是一个是“不可变容器”,另一个是“可变容器”。可变容器意味着可以向容器内增加或者删除元素,也可以修改其中某个元素,这将导致我们会将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满足typingCallable[..., 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