Python 闭包与装饰器
在大二刚刚接触Python的时候,就听说过Python的闭包和装饰器,而且很多框架和库中都用到了装饰器。装饰器会使代码更加简洁、美观,代码可读性也大大提高。初学Python时,对于闭包和装饰器的实际原理很难理解透彻,所以对他们一直是避而远之。最近看了一些关于闭包和装饰器的视频和博客,感觉自己似乎有一些理解,所以接下来就来捋一捋闭包和装饰器到底怎么回事。

闭包

​ Python有一个特点,就是万物皆对象。Python把变量、函数这些完全不一样的东西都看作是一个object。我理解的闭包就是一个返回值是函数的函数。那返回函数有什么用呢?下面先来看一个例子。

1
2
3
4
def multiple(times):
def add(a,b):
return times * (a+b)
return add

​ multiple是一个闭包函数,他返回的是一个add函数,实现两数相加,而他本身的参数是用来将相加的结果进行加倍。

1
2
add_1x = multiple(1) 
add_2x = multiple(2)

​ 当调用multiple的时候实际上我们就获得了一个add函数,随着multiple传入参数的不同,我们得到了add_1x和add_2x两个不同的函数,这两个函数实际上是不同放大不同倍数的add函数。

1
2
3
4
print(add_1x(1,1)) #output:2
print(add_2x(1,1)) #output:4
print(add_1x(3,3)) #output:6
print(add_2x(3,3)) #output:12

​ 我们可以看到add_1x输出的结果就是参数相加后的结果,而add_2x输出的结果全部变成了二倍。在我们之前的认知里,函数内部的变量时局部变量,当函数执行完毕后,函数内部的变量就会被销毁。而闭包函数实际上保存了函数调用的“案发现场”,当我们调用它返回的内部函数的时候,原来的参数依旧可以参与内部函数运算。

装饰器

​ 装饰器其实就是闭包穿了个马甲,是Python中的语法糖,他的核心逻辑其实还是闭包。他的基本形式是下面这样的:

1
2
@decorator
def func()

看起来装饰器的调用过程和函数半毛钱关系没有,但实际上它的调用过程就是decorator(func)(func_args),说白了就是把函数作为参数传入装饰器中。那装饰器究竟是怎么实现的呢?下面我们来实现记录一个函数执行时间的装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def logger(func):
import time
def wrapper(a,b):
res = func(a,b)
print("{}: 函数{}执行,执行结果->{}".format(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), func.__name__, res))
return res
return wrapper

@logger
def add(a,b):
return a+b

add(1,1)

#output: 2022-03-12 19:21:03: 函数add执行,执行结果->2

​ 我们可以看到,当用logger装饰add时,当我们执行add函数,控制台就会输出执行的时间、函数名以及结果。那具体的执行过程是怎么样的呢?

​ 首先调用add(1,1)函数时,实际上是调用了logger(add)(1,1),add函数作为参数传入logger中返回wrapper函数,就相当于调用了wrapper(1,1),将wrapper内的func替换成add函数执行了wrapper内部的函数逻辑。

​ 现在有一个问题,这个装饰器只能装饰带有两个参数的函数,一旦装饰不是两参数的函数,必然会导致wrapper接收到的参数个数与定义的不符。解决这个问题很简单,我们只需要把wrapper和内部的func调用改写成带有不定长参数形式就可以解决参数个数不符的问题了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def logger(func):
import time
def wrapper(*args,**kwargs):
res = func(*args,**kwargs)
print("{}: 函数{}执行,执行结果->{}".format(time.strftime("%Y-%m-%d %H:%M:%S",time.localtime()), func.__name__, res))
return res
return wrapper

@logger
def add1(a, b):
return a + b
@logger
def add2(a, b, c):
return a+ b + c

add1(1,1)
add2(1,1,1)

# output: 2022-03-12 19:37:15: 函数add1执行,执行结果->2
# output: 2022-03-12 19:37:15: 函数add2执行,执行结果->3

​ 在实际开发过程中,我们发现有一些装饰器往往是带参数的,那带参数的的装饰器该怎么实现呢,参数应该加在什么地方呢?我们来分析一下无参时装饰器的调用顺序。上面刚刚提到,无参是装饰器实际上是decorator(func)(func_args)这样调用的,那装饰器的参数实际上就应该是装饰器函数的参数肯定是要紧跟着装饰器的,不然函数名和参数列表就分家了肯定是不符合函数调用语法的。那么很明显正确的调用就应该是decorator(decorator_args)(func)(func_args)。这种情况下的装饰器要怎么实现呢?我们知道函数名后的括号代表的是函数的参数,一个括号就对应了一个参数。这样的话,肯定是要用三个函数才能实现带参数的闭包了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def logger(time_format):
import time
def inner(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print("{}: 函数{}执行,执行结果->{}".format(time.strftime(time_format, time.localtime()), func.__name__, res))
return res
return wrapper
return inner

@logger("%Y-%m-%d %H:%M:%S")
def add1(a, b):
return a + b

@logger("%H:%M:%S")
def add2(a, b, c):
return a+ b + c

add1(1,1)
add2(1,1,1)

# output: 2022-03-12 19:47:15: 函数add1执行,执行结果->2
# output: 19:47:15: 函数add2执行,执行结果->3

总结

​ 其实闭包和装饰器理解起来比较简单,但是他们的用法和在项目中能够发挥的作用还远不止与此,理解这些语法的内在逻辑并在实际项目中发掘和运用才是学习的最终目的。