知识链接:
装饰器
装饰器可调用的对象,其参数是被装饰的函数。装饰器可能会处理被装饰的函数然后把它返回,或者将其替换成另外一个函数或者可调用对象。
装饰器有两大特性:
- 能把被装饰的函数替换成其他函数(在元编程时,这样更方便 --> 在运行时改变程序的行为)
- 装饰器在加载模块时立即执行
把被装饰的函数替换成其它函数
这样描述可能比较抽象,我们可以从一个例子来看下其特性:如何去计算函数执行的时间?
处理思路常规的处理思路是:在函数执行开始与结束时分别记录时间,并计算差值(执行时间)。这样处理有好处,也有不便的地方。
好处:如果是计算单个函数,修改简单易处理。
不便:会修改整个函数的执行逻辑,且在大量需要计算函数执行时间的地方不方便统一修改。
我们可以通过装饰器来进行处理:
import time
def fun1():
time.sleep(3) # 模拟函数执行过程
def timmer(func):
def inner(*args, **kwargs):
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
print("函数执行时间为:", end_time - start_time)
return inner
fun1 = timmer(fun1)
fun1() # 函数执行时间为: 3.0051169395446777
上诉代码的执行效果与下述写法一样:
import time
def timmer(func):
def inner(*args, **kwargs):
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
print("函数执行时间为:", end_time - start_time)
return inner
@timmer
def fun1():
time.sleep(3) # 模拟函数执行过程
fun1() # 函数执行时间为: 3.0051169395446777
这样做有一个小问题,就是被装饰的函数的__name__和__doc__属性被遮盖了。
可以通过使用functools.wraps 装饰器把相关属性从fun复制到timmer中:
import time
import functools
def timmer(func):
@functools.wraps(func)
def inner(*args, **kwargs):
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
print("函数执行时间为:", end_time - start_time)
return inner
@timmer
def fun1():
time.sleep(3) # 模拟函数执行过程
模块加载时立即执行
再通过一个例子来理解装饰器会在模块加载时立即执行:
def register(func):
print(f"running decorator...register: {func}")
return func
@register
def fun1():
print("running func1")
@register
def fun2():
print("running fun2")
def fun3():
print("running fun3")
if __name__ == '__main__':
fun1()
fun2()
fun3()
------------------------------
running decorator...register: <function fun1 at 0x7f7900088280>
running decorator...register: <function fun2 at 0x7f7900096550>
running func1
running fun2
running fun3
可以看出:装饰器在导入模块时立即执行,而被装饰的函数只有在明确调用时才会运行。这就突出了在Python中导入时和运行时的区别。
另外,如果存在多层装饰器时,导入时的执行顺序是由内到外,运行时的执行顺序是由外到内:
def war1(func):
print("running war1")
def inner1(*args, **kwargs):
print("====inner1====")
print(f'inner1中参数func:{func}')
func(*args, **kwargs)
return inner1
def war2(func):
print("running war2")
def inner2(*args, **kwargs):
print("====inner2====")
print(f'inner2中参数func:{func}')
func(*args, **kwargs)
return inner2
@war2
@war1
def fun1():
print("running func1")
# --> fun1 = war2(war1(fun1))
if __name__ == '__main__':
fun1()
------------------------------------
running war1
running war2
====inner2====
inner2中参数func:<function war1.<locals>.inner at 0x7fad681a54c0>
====inner1====
inner1中参数func:<function fun1 at 0x7fad680e6550>
running func1
变量作用域
在理解闭包之前,我们需要先了解下变量作用域。
先来看第一个例子,这段代码很简单也很容易理解:
b = 3
def fun():
a = 1
print(a) # 获取局部变量并打印
print(b) # 获取全局变量并打印
fun()
--------------------------------
1
3
下面,再坐下简单的修改:
b = 3
def fun():
a = 1
print(a)
print(b)
b = 9
fun()
------------------
UnboundLocalError: local variable 'b' referenced before assignment
1
在示例一的基础上,增加一行为b赋值,可是在执行print(b)时却报错了,但是输出了1,这表明print(a)是正常执行的。为什么有全局变量b,且是在print(b)之后进行赋值操作的,为什么会报错没有声明就引用的错误了呢?
我们可以通过比较两个示例函数的字节码来找到原因:
import dis
print(dis.dis(fun))
-----------------------------------------------------
7 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (a)
8 4 LOAD_GLOBAL 0 (print)
6 LOAD_FAST 0 (a)
8 CALL_FUNCTION 1
10 POP_TOP
9 12 LOAD_GLOBAL 0 (print)
14 LOAD_GLOBAL 1 (b)
16 CALL_FUNCTION 1
18 POP_TOP
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
None
import dis
print(dis.dis(fun))
-----------------------------------------------------
7 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (a)
8 4 LOAD_GLOBAL 0 (print)
6 LOAD_FAST 0 (a)
8 CALL_FUNCTION 1
10 POP_TOP
9 12 LOAD_GLOBAL 0 (print)
14 LOAD_FAST 1 (b)
16 CALL_FUNCTION 1
18 POP_TOP
10 20 LOAD_CONST 2 (9)
22 STORE_FAST 1 (b)
24 LOAD_CONST 0 (None)
26 RETURN_VALUE
None
从字节码中可以看出Cpython解释器在编译示例二中的函数时,把b视作局部变量(因为在函数中为b赋值了),即使在print(b)后面才为b赋值,因为变量的种类(是不是局部变量)不能改变函数的定义体。
global
有没有一种方法可以在示例二中为全局变量赋值,又不会导致解释器报错的呢?
Python提供了global关键字,可以在函数体中声明变量为全局变量:
import dis
b = 3
def fun():
global b
a = 1
print(a)
print(b)
b = 9
print(b)
if __name__ == '__main__':
fun()
print(dis.dis(fun))
----------------------------
1
3
9
打印其字节码可以看到,Cpython解释器已经把b视为全局变量了:
8 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (a)
9 4 LOAD_GLOBAL 0 (print)
6 LOAD_FAST 0 (a)
8 CALL_FUNCTION 1
10 POP_TOP
10 12 LOAD_GLOBAL 0 (print)
14 LOAD_GLOBAL 1 (b)
16 CALL_FUNCTION 1
18 POP_TOP
11 20 LOAD_CONST 2 (9)
22 STORE_GLOBAL 1 (b)
24 LOAD_CONST 0 (None)
26 RETURN_VALUE
None
闭包
闭包指延展了作用域的函数,其中包含函数定义体中的引用,但是不在定义体中定义的非全局变量。
在装饰器的示例中,存在一个疑问:func是timmer函数的局部变量,在timmer(func1)执行完后,其本地作用域也会清除,为什么在执行func1()时可以获取到对应的func参数呢?
import time
def fun1():
time.sleep(3)
def timmer(func):
def inner(*args, **kwargs):
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
print("函数执行时间为:", end_time - start_time)
return inner
fun1 = timmer(fun1)
fun1()
在inner函数中,func是自由变量(free varialbe),指未在本地作用域中绑定的变量。审查fun1的__code__属性可以看到这些值,自由变量的值可以在__closure__中查看:
>>> fun1.__code__.co_varnames
('args', 'kwargs', 'start_time', 'end_time')
>>> func1.__code__.co_freevars
('func',)
>>> fun1.__closure__[0].cell_contents
<function fun1 at 0x7fc0580e8040>
nonlocal
我们接下来再看一个例子,通过闭包来计算移动的平均值的高阶函数:
def make_average():
total_step = 0 # 移动总数
count = 0 # 移动次数
def average(new_step):
count += 1
total_step += new_step
return total_step / count
return average
if __name__ == '__main__':
avg = make_average()
print(avg(10)) # 模拟移动
print(avg(20))
-------------------------------------
UnboundLocalError: local variable 'count' referenced before assignment
通过对闭包的学习,这段代码应该没问题,但是执行却报错了。正常来说count和total_step都应该是自由变量,为什么会被Cpython解释器视为局部变量呢【在上一节可以找到答案】?
为了解决这个问题,Python3引入了nonlocal声明,它的作用是把变量声明为自由变量,即使如果为闭包中绑定的自由变量赋予了新值则会更新绑定:
def make_average():
total_step = 0 # 移动总数
count = 0 # 移动次数
def average(new_step):
nonlocal count, total_step
count += 1
total_step += new_step
return total_step / count
return average
if __name__ == '__main__':
avg = make_average()
print(avg(10)) # 模拟移动
print(avg(20))
---------------------------------------
10.0
15.0
查看字节码可以看到count和被声明为自由变量:
7 0 LOAD_DEREF 0 (count)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_DEREF 0 (count)
8 8 LOAD_DEREF 1 (total_step)
10 LOAD_FAST 0 (new_step)
12 INPLACE_ADD
14 STORE_DEREF 1 (total_step)
9 16 LOAD_DEREF 1 (total_step)
18 LOAD_DEREF 0 (count)
20 BINARY_TRUE_DIVIDE
22 RETURN_VALUE
None
函数是一等对象
在Python中,函数是一等对象:
- 在运行时被创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传递给函数
- 能做为函数的返回结果
- 后面亮点可以为我们理解闭包提供帮助
装饰器面试题
-
装饰器基础知识考察
-
装饰器作用
-
装饰器的原理和实现
-
装饰器存在的缺陷
-
工作中是否有用到装饰器(Python内置的或第三方封装的)
-
内置
-
property、classmethod、staticmethod
-
functools.wraps
-
functools.lru_cache
-
functools.singledispatch
-
第三方
-
Django的csrf_exempt
-
手写装饰器
-
装饰器进阶
-
导入时和运行时
-
多层装饰器执行顺序
-
自由变量