【摘要】
大多数编程语言都提供了一些奇技淫巧,通常叫做语法糖,对于语法糖的定义如下:
1.计算机语言中特殊的某种语法
2.这种语法对语言的功能并没有影响
3.对于编程人员有更好的易用性
4.能够增加程序的可读性
Python中同样也有很多语法糖,比如:列表推导式,with上下文管理器,装饰器等。其中装饰器是一个十分重要的特性,并且在之前面向对象章节中类的讲解,以及Flask框架的讲解中,也都或多或少使用过装饰器。
今天这一章节,就来着重给大家讲解一个Python中的装饰器。
【定义】
装饰器是从Decorator直译而来,它可以在不改变一个函数代码和调用方式的情况下给函数添加新的功能。
装饰器本质上是一个闭包函数,它接受被装饰的函数(func)作为参数,并返回一个包装过的函数。这样我们可以在不改变被装饰函数的代码的情况下给被装饰函数或程序添加新的功能。
Python的装饰器广泛应用于缓存、权限校验(如Flask中的@login_required和@permission_required装饰器)、性能测试(比如统计一段程序的运行时间)和插入日志等应用场景。
有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码,增加一个函数的重用性。
先看一个最简单的装饰器例子,代码如下:
import time
def timer(func):
def inner(num):
start = time.time()
func(num)
end = time.time()
print("%s cost %f seconds" % (func.__name__, end - start))
return inner
@timer
def foo(num):
time.sleep(num)
if __name__ == "__main__":
foo(3)
# 运行程序后输出
# foo cost 3.000634 seconds
上述代码中的timer
函数就是一个装饰器函数,它接收另外一个函数作为参数,并统计其运行的时间。
foo
函数是定义的一个示例函数,在其定义语句上面增加一行@timer
就是给它增加了一个装饰器。
所以@
其实就是Python中装饰器的语法糖。
【闭包】
装饰器函数其实本质上就是一个闭包函数,上面代码示例中的timer
就是一个闭包函数。
闭包是Python编程一个非常重要的概念。如果一个函数中定义了一个内函数,且内函数体内引用到了体外的变量,这时外函数通过return返回内函数的引用时,会把定义时涉及到的外部引用变量和内函数打包成一个整体(闭包)返回。
闭包的定义可能会有点儿晦涩难懂,我们通过下面的代码示例详细解释一下:
def outer(a):
def inner(b):
print(a + b)
inner(2)
outer(2)
# 输出结果为 3
上面代码中的inner
就是内函数,内函数中定义了一个局部变量b
,除此之外还引用到了其函数外部的a
变量。
a
变量本身不属于inner
函数的变量作用域,但如果inner
函数使用a
变量时无法在自己的局部作用域中找到该变量时,会继续向上一层作用域中寻找,而inner
函数的上一层作用域就是outer
函数,a
变量就是outer
函数的参数,所以此时inner
函数就找到了a
变量。
当调用outer()
函数时,其实outer
内部又调用了inner
函数,但如果在outer
函数里不调用inner
函数,而是返回inner
函数的引用,会有什么效果呢?代码如下:
def outer(a):
def inner(b):
print(a + b)
return inner
f = outer(2)
print(type(f))
print(f)
print(f.__name__)
# 输出结果为
<class 'function'>
<function outer.<locals>.inner at 0x7f9fd8093280>
inner
从代码可以看出,执行f = outer(2)
之后,并没有任何输出结果,内部也没有运行inner
函数,而是将inner
函数的引用返回了出来,所以f
就是inner
函数的引用。
如果想要调用inner
函数,就需要如下写法:
def outer(a):
def inner(b):
print(a + b)
return inner
f = outer(2)
f(1)
# 输出结果为 3
# 或者写成 outer(2)(1)
光从变量的作用域中仍无法体现闭包的精髓。
正常情况下,一个函数运行结束的时候,临时变量会被销毁,所以上面代码中执行了f = outer(2)
,此时其实outer
函数已经执行结束了,所以outer
函数中的局部变量a
理应被销毁。
但由于我们在outer
函数中返回了inner
函数的引用,而inner
函数又使用了outer
函数中的局部变量,所以执行完f = outer(2)
后,还不能将被inner
函数引用到的变量a
销毁,而是会将outer
函数的局部变量同inner
函数绑定在一起,并将inner
函数返回,以供后续调用。
所以outer
函数中最后的return inner
做了两件事情,一件是返回了inner
函数的引用,另一件就是暂时保留了被inner
函数引用到的外部变量。
综上所述,闭包就是即使外函数已经结束了,内函数仍然能够使用外函数的临时变量。
【装饰器】
在了解了闭包之后,就可以开始真正了解Python中的装饰器了。
计算一个函数执行时间的闭包代码如下所示:
import time
def timer(func):
def inner(num):
start = time.time()
func(num)
end = time.time()
print("%s cost %f seconds" % (func.__name__, end - start))
return inner
timer
函数是一个外函数,并接受一个参数func
,inner
函数是一个内函数,在inner
函数中使用了timer
的func
变量;最终返回了inner
函数的引用。
比较特殊的是timer
函数可以接受的是一个函数的引用,所以我们可以做如下事情:
def foo(num):
time.sleep(num)
timer(foo)(3)
# 输出结果为 foo cost 3.000634 seconds
timer(foo)(3)
这种写法总让人感觉很奇怪,易读性很低,而且编程人员写起来也不优雅。这时候就到了语法糖发挥作用的时候了。
Python中@
就是一个语法糖,它的作用就是将被装饰函数的引用当作参数传入装饰函数中,代码如下:
import time
def timer(func):
def inner(num):
start = time.time()
func(num)
end = time.time()
print("%s cost %f seconds" % (func.__name__, end - start))
return inner
@timer
def foo(num):
time.sleep(num)
foo(3)
# 输出结果为 foo cost 3.000634 seconds
所以foo
函数被装饰后,在调用foo
函数时,会先将foo
函数的引用传入了timer
函数中,再执行timer
函数的返回值,等价于timer(foo)(3)
。
【总结】
装饰器始终是Python中十分重要的一个语法糖,可以说随处可见,这一章节主要从实现的角度给大家讲解了一下装饰器,也是希望大家能够在Python给我提供的便利下,窥见更多实现原理和细节,并且这种深入的理解对以后的编程也会起到很大的作用。
下一章节我会讲解规范的Python代码中是如何使用装饰器的,并且带领大家对更深一步的源码进行阅读,敬请期待。
欢迎大家添加我的个人公众号【Python玩转自动化运维】加入读者交流群,获取更多干货内容