Python函数注释

Python 函数注释是在PEP 3107引入的,其主要目的是给函数形参和返回值添加可选元数据。

两点重要的信息

  1. 函数注释可选
  2. 可添加任意 Python 表达式,但是 Python 本身并不会对表达式做处理,也不具有任何意思

函数注释语法如下:

1
2
3
4
5
>>> def annotation(param1: "第一个函数注释", param2: "第二个函数注释") -> "返回值函数注释":
... pass
...
>>> annotation.__annotations__
{'param1': '第一个函数注释', 'param2': '第二个函数注释', 'return': '返回值函数注释'}

我们可以通过函数对象的__annotation__ 属性来获取所有的注释,其中有一个特别的 key 是 return 专门用来存放返回值函数注释。

函数注释可以被用来实现很多功能:

  • 类型检测
  • 谓词逻辑
  • IDE 类型辅助

下面我们利用函数注释实现一个简单的谓词逻辑功能。

我们先创建一个函数,并给两个参数添加_谓词注释_。

1
2
def repeat(times: 'times > 0 and times < 10', name: 'len(name) > 5') -> str:
return name * times
  1. times 必须大于 0 并且小于 10
  2. name 长度必须大于 5

我们可以直接运行该函数,但是 Python 不会解释注释,所以注释并没有效果。接下来我们来学习如何优雅的访问这些注释。

1
2
3
4
5
6
7
8
from inspect import signature

sig = signature(repeat)
for param_name in sig.parameters:
print(f"{sig.parameters[param_name]}")

# times: 'times > 0 and times < 10'
# name: 'len(name) > 5'

我们可以利用signature 来访问函数参数,注意signature.parameters 是一个有序字典,按照参数列表的顺序访问参数。

接下来我们要使用装饰器语法来定义一个可以再运行时访问实参的 wrapper 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def parameter_predicate(f):
def wrapper(*args, **kwargs):
for arg in args:
print(arg)
for key, value in kwargs:
print(key, value)

return f(*args, **kwargs)
return wrapper

@parameter_predicate
def repeat(times: 'times > 0 and times < 10', name: 'len(name) > 5') -> str:
return name * times

repeat(1, "abcde")
# 1
# abcde

现在唯一欠缺的就是如何执行注释中的 Python 表达式,我们使用eval内置函数

1
2
3
globals = {}
locals = {"name": "Hello, World"}
eval("print(name)", globals, locals)

eval函数可以执行任意的Python 片段,其中出现的变量会通过globalslocals 变量表查询,如果没有查询到则会报错。

现在我们有了实现谓词解释器的所有工具。我们可以组合这些代码如下所示。

由于 Python 中参数可以通过位置或者关键字的形式传入,所以argskwargs都需要处理,我们将实参值做为locals变量表传给eval,并且将失败的失败的谓词逻辑放入failures数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from inspect import signature


def parameter_predicate(f):
sig = signature(f)

def wrapper(*args, **kwargs):
params = [param for param in sig.parameters]
failures = []
for i in range(0, len(args)):
param = sig.parameters[params[i]]
value = args[i]
locals = {param.name: value}
globals = {}
if not eval(param.annotation, globals, locals):
failures.append({
"name": param.name,
"condition": param.annotation.format(param.name),
"context": {param.name: value}
})
for i in range(len(args), len(args) + len(kwargs)):
param = sig.parameters[params[i]]
value = kwargs[param.name]
locals = {param.name: value}
globals = {}
if not eval(param.annotation, globals, locals):
failures.append({
"name": param.name,
"condition": param.annotation.format(param.name),
"context": {param.name: value}
})
if len(failures) > 0:
raise RuntimeError(f"{failures}")
return f(*args, **kwargs)

return wrapper

@parameter_predicate
def repeat(times: 'times > 0 and times < 10', name: 'len(name) > 5') -> str:
return name * times

repeat(1, "abcde")
# RuntimeError: [{'name': 'name', 'condition': 'len(name) > 5', 'context': {'name': 'abcde'}}]
print(repeat(0, "abcdef"))
# RuntimeError: [{'name': 'times', 'condition': 'times > 0 and times < 10', 'context': {'times': 0}}]

通过定义并处理函数注释,我们实现了一个简单的谓词解释器,帮助我们附加一些谓词条件到形参上。当然通过自定义一些语法,我们可以是谓词更加复杂具有更多功能。