浅析Python中的闭包与装饰器

Python中的装饰器经常用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。本文从闭包的概念引入,并以实例形式对装饰器及其应用进行了讲解。

Python变量作用域

在了解闭包之前,我们得先了解一下python解释器查找变量时的规则。在python中,查找一个变量名称的顺序为local-->enclosing function locals-->global-->builtin,简称LEGB。它们各自的含义如下:

  1. Local - 当前所在命名空间(如函数、模块),函数的参数也属于命名空间内的变量
  2. Enclosing - 外部嵌套函数的命名空间
  3. Global - 全局变量,函数定义所在模块的命名空间
  4. Builtin - 内置模块的命名空间
    举例说明:
    1
    2
    3
    4
    5
    6
    7
    8
    val1=0
    def fun(val2):
    def Max():
    return max(val1, val2)
    return Max()
    print(fun(1))
    # 输出:1

在本例中,val1即为Global;而val2,对函数fun而言为Local,对嵌套的函数Max而言就是Enclosing;小写的函数max并没有定义,但我们可以直接使用,因为它是python标准库里的函数,即为Builtin。

闭包的概念

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数,它是将组成函数的语句和这些语句的执行环境打包在一起时,得到的对象,即闭包=函数块+定义函数时的环境。在Python中,一切皆对象,函数也是一个对象,它可以当作一个参数被传入,也可以作为一个结果被返回。Python以函数对象为基础,为闭包提供了语法支持。现在举个例子说明一下:

1
2
3
4
5
6
7
8
def a(m):
def b(n):
print(m,n)
return b
c=a('hello')
c('test1') # 输出:hello test1
c('test2') # 输出:hello test2

在这个例子中,调用函数a时就产生了一个闭包b,并且该闭包拥有enclosing变量m,通过最后两个测试的例子可以看出,在函数a执行完成后,变量m依然存在,这是因为它被闭包b引用了,所以不会被回收。另外可以看出,本例中闭包其实就是一个引用了enclosing变量m的函数。

装饰器

装饰器,就是将被装饰的函数当作参数传递给与装饰器对应的函数(名称相同的函数),并返回包装后的被装饰的函数。在Python中,装饰器被用于用@语法糖修辞的函数或类。装饰器有很多作用,一种常用的方法是将其作为一种函数调用日志记录器。现举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def log(func):
def wrapper(*args, **kw):
print(func.__name__ + ' is running')
return func(*args, **kw)
return wrapper
@log
def fun():
print('hello')
if __name__ == '__main__':
fun()
'''
执行结果:
fun is running
hello
'''

在本例中,log函数返回了一个闭包wrapper,它引用了一个变量func。而定义函数fun时的@log,即可看作fun=log(fun),@log只是一种更直观的写法。而装饰器即为对闭包的一种应用,只不过它传递的变量是函数罢了。
装饰器本身接收一个函数作为参数,但是有时候我们需要装饰器接受另外的参数,此时需要在外层再加一层函数,修改上例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print(text + func.__name__ + ' is running')
return func(*args, **kw)
return wrapper
return decorator
@log('test:')
def fun():
print('hello')
if __name__ == '__main__':
fun()
print(fun.__name__)
'''
执行结果:
test:fun is running
hello
fun
'''

这里的log函数即为一个带参数的装饰器,它其实是对原有装饰器的一种封装。使用装饰器的一个缺点是无法保存原有函数的信息,如本例中的__name__属性,使用python内置的functools即可解决这个问题,可以看到本例最后的输出依然为fun,如果不使用functools,最后的输出将为wrapper。
通过上述两例可以看出,一个函数不用做任何修改,只需要在定义的地方加上装饰器,调用的方式也不用做任何改变,即可完成对函数功能的增强。如果代码中含有大量类似的函数,那么我们就可以直接在定义函数的地方加上装饰器,而不必修改每一个函数,这样不仅可以提高程序的可复用性,也可以提高程序的可读性。

装饰器的应用

装饰器其实就是一个包装函数的函数,它可以为已经存在的对象添加额外的功能,因此经常被用于有切面需求的场景,较为经典的有插入日志、 性能测试、事务处理等。比如在python的轻量级web开发框架flask中,大量地使用装饰器,用flask开发的代码简洁而又优雅,极具python的风格。一个flask的DEMO如下:

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()

flask的默认端口为5000,使用浏览器打开http://localhost:5000/,即可看到结果。在此例中,flask使用app.route装饰器将一个url绑定到对应的函数,非常简单而又直观。

总结

Python的装饰器本质上是对闭包的一种应用,但它不仅可以用函数实现,也可以用类实现,并且还可以一次性使用多个装饰器。虽然定义起来有点复杂,但使用起来却非常灵活和方便。它可以极大地增强函数的功能,同时又不会增加调用者的负担,维护起来也非常地容易。