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}}]

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

Python 中的装饰器

Python 中的装饰器

首先,假设我们有一个函数get_remote_resource接受一个参数res_id并且返回要获取取的后台资源。
有一个函数get_resource_x_png,获取一个名为x.png的图片。

1
2
3
4
5
6
def get_remote_resource(res_id):
...

def get_resource_x_png():
res_id = 'x.png'
return get_remote_resource(res_id)

这时,假设我们引入了缓存技术,那么自然在获取后台资源之前,我们需要检测当前资源是否在缓存中存在。

1
2
3
4
5
6
7
8
9
10
11
12
def get_remote_resource(res_id):
...

def exists_in_cache(res_id):
return res_id in cache

def get_resource_x_png():
res_id = 'x.png'
if exists_in_cache(res_id):
return cache[res_id]
else:
return get_remote_resource(res_id)

如上代码所示,我们直接在get_resource_x_png中加入缓存检测的逻辑。
这样的代码会导致获取资源的逻辑和检测缓存的逻辑之间出现紧耦合的现象。
那么也就无法在没有缓存的环境下重用get_resource_x_png,导致函数变得不灵活。
最好的我们能通过某种手段剥离缓存检测的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_remote_resource(res_id):
...

def exists_in_cache(func):
def wrapper():
res_id = 'x.png'
if res_id in cache:
return cache[res_id]
else:
return func()
return wrapper

@exists_in_cache
def get_resource_x_png():
res_id = 'x.png'
return get_remote_resource(res_id)

如上述代码所示,我们采用了一种装饰器的技巧。
其中exists_in_resource就是一个装饰器,它接受一个函数func作为参数,并且返回一个函数wrapper
wrapper会检查x.png在缓存中是否存在,如果存在则返回缓存中的结果,如果不存在这调用func并返回结果。

其中@exists_in_cache是 Python 的一种语法糖,其效果类似于调用exists_in_cache(get_resource_x_png)并将返回的函数重新赋值给get_resource_x_png
这样在使用时,可以直接使用同一个函数名字,不增加命名负担。

1
get_resource_x_png = exists_in_cache(get_resource_x_png)

如果对于装饰器本身如何工作不理解的话,请查阅 Python 中的闭包


假设我们希望修改函数get_resource_x_png,通过传递资源 ID,获取对应的资源。
由于之前的装饰器在调用被装饰函数时并没有传递参数,所以会导致调用失败,造成异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_remote_resource(res_id):
...

def exists_in_cache(func):
def wrapper(*args, **kwargs):
res_id = kwargs['res_id'] if 'res_id' in kwargs else args[0]
if res_id in cache:
return cache[res_id]
else:
return func(*args, **kwargs)
return wrapper

@exists_in_cache
def get_resource(res_id):
return get_remote_resource(res_id)

这时,我们就需要修改装饰器内部的wrapper函数, 添加argskwargs接受任意参数,并且将接收到的参数传递给被装饰的函数get_resource
res_id可以通过判断外部的传参方式来获取,如果是关键字传参字,则直接获取;如果不是,则通过 args 元组获取参数。


假设,我们又有一个新函数get_resource_by_type,改函数使用的缓存是另一个缓存type_cache
那么我们该如何通过修改装饰器来使代码保持一致呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def get_remote_resource(res_id):
...

def exists_in_cache(cache):
def decorator(func):
def wrapper(*args, **kwargs):
res_id = kwargs['res_id'] if 'res_id' in kwargs else args[0]
if res_id in cache:
return cache[res_id]
else:
return func(*args, **kwargs)
return wrapper
return decorator

@exists_in_cache(cache)
def get_resource(res_id):
return get_remote_resource(res_id)

@exists_in_cache(type_cache)
def get_resource_by_type(res_type):
...

我们用一个高级一点的带参数装饰器
现在exists_in_cache是一个函数接受一个cache作为参数,并且返回一个decorator
这个decorator的构成和上一个代码中的exists_in_cache函数一致。

那么这里发生了一些什么呢?
其实带参数装饰器也是一种语法糖,等效于如下代码。
先调用exists_in_cache(type_cache),生成一个新的装饰器(注意,此时内部的cache被绑定到了传递进来的type_cache),然后生成的装饰器再用get_resource_by_type作为参数生成最终的get_resource_by_type

1
get_resource_by_type = exists_in_cache(type_cache)(get_resource_by_type)

通过装饰器等价的代码,我们可以理解到,由装饰器产生的函数get_resource包含的元信息(函数名get_resource.name,函数文档get_resource.doc)和我们定义的get_resource不一样了。
原因是实际上我们调用的不是get_resource而是在装饰器内部定义的wrapper函数,自然函数元信息会不一样。
考虑到有些情况下,会处理元信息,为了保留该元信息。
Python 为我们引入了functools.wraps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_remote_resource(res_id):
...

def exists_in_cache(cache):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
res_id = kwargs['res_id'] if 'res_id' in kwargs else args[0]
if res_id in cache:
return cache[res_id]
else:
return func(*args, **kwargs)
return wrapper
return decorator

@exists_in_cache(cache)
def get_resource(res_id):
return get_remote_resource(res_id)

@exists_in_cache(type_cache)
def get_resource_by_type(res_type):
...

如此一来即使是装饰后的函数也会包含一样的元信息。


最后补充一点,装饰器是可以叠加任意层数的,类似如下:

1
2
3
4
5
@C
@B
@A
def f():
...

由于装饰的顺序不同可能产生的效果也会不同,所以使用时需要认真考虑。

Python 中的闭包

Python 中的闭包

问题

假设我们有一个log函数用于向终端输出一条信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
def log(msg):
print(msg)

def task1():
log('processing task')
...

def task2():
log('processing task')
...

task1() # processing task
task2() # processing task

两个函数task1,tasks2都调用了log函数输出了相同的log信息。
让我们很难定位到底哪一个是task1输出的,哪一个是task2输出的。
那怎么办?

显然我们可以给log信息添加一个前缀信息,例如:

1
2
[task1]: processing task
[task2]: processing task

实现方法

方案 1

我们可以修改log函数签名,增加一个参数prefix。但是这么做的问题是,log函数的签名被修改了,那么所有引用该函数的地方都需要修改,对于大型工程项目会造成相当大的工作量。

方案 2

使用闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def log(msg):
print(msg)

def log_generator(prefix):
def wrapper(msg):
log('[{}]: {}'.format(prefix, msg))
return wrapper

log1 = log_generator(prefix='task1')
log2 = log_generator(prefix='task2')

def task1():
log1('processing task')
...

def task2():
log2('processing task')
...

task1() # [task1]: processing task
task2() # [task2]: processing task

如以上代码所示,我们通过log_generator可以生成拥有任意前缀的信息的log函数,而不改变log函数本身的签名。
这个技巧或者概念被称为闭包

闭包概念

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.

在编程语言中,一个闭包,也被称为语法闭包或者函数闭包,是一种在支持头等函数的语言中实现静态范围命名绑定的技巧。闭包是一种结构体,存储一个函数以及相关的环境。该环境是一个映射关系,把函数里面自由变量(本地使用,但是在外部定义)与闭包创建时变量名对应的值或者引用绑定在一起。

简单来说,闭包就是一个函数和一个“字典”,该“字典”将闭包创建时函数所引用的_外部变量_变成键值对。

1
2
3
4
5
6
7
8
9
10
def log(msg):
print(msg)

def log_generator(prefix):
def wrapper(msg):
log('[{}]: {}'.format(prefix, msg))
return wrapper

log1 = log_generator(prefix='task1')
log2 = log_generator(prefix='task2')

log_generator函数为例,在创建log1时,我们创建了一个闭包,该闭包有一个函数wrapper和一个“字典”组成,该字典有一个键值对prefix -> task1。同理在log2闭包的字典里,有一个键值对prefix -> task2

应用场景

装饰器

1
2
3
4
5
6
7
8
9
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def func(msg):
print(msg)

func = decorator(func)

如上述样例代码所示,我们可以利用decorator函数做任何事情,例如打印 log,记录信息,修改参数等等。可以再不修改原有函数的情况下,增加很多有用的信息。

1
2
3
4
5
6
7
8
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@decorator
def func(msg):
print(msg)

Python 使用语法糖@decorator帮助减少最后一条赋值代码。