7、闭包、装饰器
1、闭包
1.1、函数的嵌套与作用域的理解
重点:
- 嵌套函数各变量的作用域:全局(
Global
)、外部嵌套(Enclosing Local
)、局部(Local
) - 函数对象赋值给变量,变量转化为函数对象,如:
v=f1
- 调用函数对象,如:
v()
- 如果不把内层函数作为外层函数的返回值,那么无法调用内层函数。
示例:
a=90 # 全局变量,作用域全局(Global)
def f1(): # f1是外层函数
b=80 # b是外部嵌套函数变量(Enclosing Local)
def f2(): # f2是内层函数
c=70 # c是局部变量(Local)
print(a) # f2内部调用a变量 ---> Yes
print(b) # f2内部调用b变量 ---> Yes
print(c) # f2内部调用c变量 ---> Yes
f2() # 在f1中调用f2函数
print(a) # f1内部调用a变量 ---> Yes
print(b) # f1内部调用b变量 ---> Yes
print(c) # f1内部调用c变量 ---> No 因为c是f2内部的局部变量,在f1中不能调用
v=f1 # 将f1函数对象赋值给变量v,v就转化成函数对象
v # 看下v是否是函数对象
# <function __main__.f1()>
v() # v()才是调用函数对象,由下面的结果可以看出,print(c)确实是错误的
# 结果:
# 90
# 80
# 70
# 90
# 80
# ---------------------------------------------------------------------------
# NameError Traceback (most recent call last)
# <ipython-input-8-e31e1deaac88> in <module>
# ----> 1 v()
# <ipython-input-5-27835136cdb7> in f1()
# 13 print(a) # f1内部调用a变量 ---> OK
# 14 print(b) # f1内部调用b变量 ---> OK ;注意f1不能调用c
# ---> 15 print(c)
# NameError: name 'c' is not defined
- 注意:全局无法引用外部嵌套变量与局部变量,以及局部函数
print(b)
# NameError: name 'b' is not defined
print(c)
# NameError: name 'c' is not defined
f2()
# NameError: name 'f2' is not defined
示例:
我们再来看下面一个嵌套函数
def print_msg(): # 外层函数
msg = "zen of python"
def printer(): # printer是内层函数
print(msg)
return printer # 将内层函数作为返回值
v=print_msg # 将外层函数的函数对象赋值给v,v转化为函数对象
v # v的类型,由结果可以看出v是函数对象
# <function __main__.print_msg()>
v() # 相当于调用外层函数print_msg
# zen of python
v()() # 相当于调用内层函数printer,报错
# TypeError: 'NoneType' object is not callable
思考:
由上面的例子,我们可以看到,上面的嵌套函数无法调用内部函数,即v()()
报错。
那么有没有办法把内部函数调用出来呢?答案是有的,只需要将内层函数作为外层函数的返回结果即可。
示例:
我们在上个示例的基础上,修改成:将内层函数作为外层函数的返回值
def print_msg(): # 外层函数
msg = "zen of python"
def printer(): # printer是内层函数
print(msg)
return printer # 将内层函数作为返回值
v=print_msg # 将外层函数的函数对象赋值给v
v # 外层函数的函数对象
# function __main__.print_msg()>
v() # 内层函数的函数对象
# <function __main__.print_msg.<locals>.printer()>
v()() # 调用内层函数
# zen of python
思考:
由上面的例子,我们可以看到,如果将内层函数作为外层函数的返回值,则可以直接在全局调用内层函数,即v()()
不会报错。
1.2、nonlocal
关键字
global
关键字,用来重新赋值全局变量nonlocal
关键字,用来重新赋值外部嵌套变量
示例:
def f1():
a = 3
def f2():
nonlocal a # 使用 nonlocal 声明 a, 便可以在嵌套的函数内部修改 a 的值
a = a + 1
print(a)
f2()
f1()
# 4
a
# 3
1.3、闭包
在1.1的最后一个示例中,有三个要点,满足这三个要点的,即是闭包
- 在一个外层函数中定义了一个内层函数(即:嵌套)
- 内函数里运用了外函数的临时变量(如示例3中的
msg
变量) - 并且外函数的返回值是内函数的引用
闭包的定义
如果在一个内部函数里,对在外部函数内(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)
一般情况下,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。
示例:
def outer(a): # outer是外层函数 a和b都是外层函数的临时变量
b = 10
def inner(): # inner是内层函数
print(a+b) # 在内层函数中,用到了外层函数的临时变量
return inner # 外层函数的返回值是内层函数的引用
# 在这里我们调用外层函数传入参数5
# 此时外层函数两个临时变量 a是5 b是10 ,并创建了内层函数,然后把内层函数的引用返回存给了demo
# 外层函数结束的时候发现内层函数将会用到自己的临时变量,这两个临时变量就不会释放,会绑定给这个内层函数
demo = outer(5)
# 我们调用内层函数,看一看内层函数是不是能使用外层函数的临时变量
# demo存了外层函数的返回值,也就是inner函数的引用,这里相当于执行inner函数
demo()
# 15
demo2 = outer(7)
demo2()
# 17
2、装饰器
装饰器其实就是一个闭包,在我们了解了闭包的概念后,会比较容易了解装饰器
2.1、引入
假如我们已经有了一个统计多个参数之和的函数,如下:
def f1(*args):
sum=0
for i in args:
sum=sum+i
return sum
那么,现在我们需要额外加个需求,就是求出上面这个f1()
函数的运行时间,一般的做法如下:
import time
def f1(*args):
start_time=time.time()
sum=0
for i in args:
sum=sum+i
time.sleep(1) # 由于运行时间过短,故意加上暂停时间
run_time=time.time()-start_time
print("程序运行时间为:%f" %run_time)
return sum
f1(5,4,3,2,1)
# 程序运行时间为:1.011736
# 15
那如果有多个函数需要统计运行时间的话,按照上面的方法,需要每个函数在其内部去写时间相关的程序,会非常麻烦。如下的函数根据不同条件返回不同结果时,就更加麻烦了。
import time
def my_abs(x):
start_time=time.time()
if x<=0:
time.sleep(1) # 由于运行时间过短,故意加上暂停时间
run_time=time.time()-start_time
print("程序运行时间为:%f" %run_time)
return -x
else:
time.sleep(2) # 由于运行时间过短,故意加上暂停时间
run_time=time.time()-start_time
print("程序运行时间为:%f" %run_time)
return x
my_abs(-1000)
# 程序运行时间为:1.013927
# 1000
my_abs(500)
# 程序运行时间为:2.013115
# 500
2.2、装饰器
从上面的例子中,我们可以看出如果每个程序都加入计时程序,会特别麻烦,那有什么解决方案没有?答案是有的,这就是装饰器
示例1:
我们先定义一个计时的函数(闭包),计时函数的参数为函数
def calculate_run_time(func): # 参数为函数
def infunc():
start_time=time.time()
func() # 执行外层函数的参数,即执行参数函数
time.sleep(1) # 由于运行时间过短,故意加上暂停时间
run_time=time.time()-start_time
print("程序运行时间为:%f" %run_time)
return infunc
上面这个就是一个简单的装饰器,那么如果使用呢?以下面的f2()
函数作为参数为例:
# 定义f2函数
def f2():
print("this is f2")
将f2()
函数当成参数传入calculate_run_time()
这个装饰器
s=calculate_run_time(f2) # 将内层函数的函数对象赋值给s,注意f2后面是不跟()的
验证s
是不是函数对象
s
# <function __main__.calculate_run_time.<locals>.infunc()>
现在已经将f2函数对象以参数的形式传入内层函数(s
或infunc
)了,执行调用s
这个函数对象
s()
# 结果如下:
# this is f2
# 程序运行时间为:1.004466
@语法糖
在我们使用装饰器的时候,可以直接使用@
字符调用装饰器,这样就比上面的调用方法要简单很多
@calculate_run_time
def f2():
print("this is f2")
f2()
# 结果是:
# this is f2
# 程序运行时间为:1.008183
示例2:
在示例1中的f2()
函数是没有参数的,那如果被引用(被装饰)的函数是有参数的时候,示例1中的calculate_run_time()
装饰器能否执行呢?
@calculate_run_time
def my_abs(x):
if x<=0:
return -x
else:
return x
my_abs(-1000)
# 结果是:
# ---------------------------------------------------------------------------
# TypeError Traceback (most recent call last)
# <ipython-input-40-a6d170de51ab> in <module>
# 6 return x
# 7
# ----> 8 my_abs(-1000)
# TypeError: infunc() takes 0 positional arguments but 1 was given
# 意思就是:infunc()内没有参数,但是调用时给出了1个参数
从这我们可以看到,定义的calculate_run_time()
装饰器不能装饰有参数的函数。我们可以将calculate_run_time()
装饰器改写成如下形式:
def calculate_run_time(func):
# wrapper是约定俗成的写法,与上面的infunc一样的。
# 在这里加入位置可变参数与可变关键字参数,因为现在我们也不知道被传入的函数的参数是什么形式的
def wrapper(*args,**kwargs):
start_time=time.time()
# 被装饰的函数也传入可变参数
func(*args,**kwargs)
time.sleep(1) # 由于运行时间过短,故意加上暂停时间
run_time=time.time()-start_time
print("程序运行时间为:%f" %run_time)
return wrapper
如下,这样就能装饰有参数的函数了。但是还有一个问题,下面的例子中,结果只打印了执行时间,并没有将1000这个结果返回。所以我们的装饰器还得在继续修改。
@calculate_run_time
def my_abs(x):
if x<=0:
return -x
else:
return x
my_abs(-1000)
# 结果是:
# 程序运行时间为:1.005791
示例3:
在示例2的基础上,继续修改装饰器,即在内层函数设定一个变量result,用来在内层函数返回被引用函数的结果。
def calculate_run_time(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
time.sleep(1)
run_time = time.time()-start_time
print("程序运行时间为:%f" % run_time)
return result
return wrapper
@calculate_run_time
def my_abs(x):
if x<=0:
return -x
else:
return x
my_abs(-1000)
# 结果是:
# 程序运行时间为:1.009868
# 1000
到了这里,才最终完成我们想要的结果。即定义一个装饰器,可以用来统计函数的运行时间。
2.3、总结
- 装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象
- 装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用
- 概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能