一、基本概念和作用
1. 装饰器
装饰器(decorate)必须是可调用对象(Callable),其参数是一个函数,称为“被装饰函数”,其输出也是一个函数(或者可调用对象)。一句话:装饰器是处理函数的函数。
2.装饰器的功能
装饰器的功能是,把一个函数转换成另一个函数。假设存在一个定义好的装饰器decorate,那么使用该装饰器的方法如下:
@decorate
def target():
print('running target()')
上面代码等价于
def target():
print('running target()')
target = decorate(target)
3.定义第一个装饰器
def deco(func):
def inner():
print("耗子尾汁")
return inner
# 装饰器将target替换成了inner
@deco
def target():
print("年轻人,不讲武德!")
target()
输出:
耗子尾汁
二、装饰器的执行时机
装饰器在“被装饰函数”定义后立即执行,通常是在导入python模块时完成执行。
def deco(func):
print("没错!我装饰器已经执行了.")
return func
@deco
def target():
print("年轻人,不讲武德!")
@deco
def target1():
print("五连鞭")
输出:
没错!我装饰器已经执行了.
没错!我装饰器已经执行了.
三、闭包与nonlocal
1. 作用域
b = 6
def f(a):
print(a)
print(b)
b = 9
f(3)
上面的代码在输出3以后会报错,即输出为
3
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-13-83191ed4d718> in <module>()
4 print(b)
5 b = 9
----> 6 f(3)
<ipython-input-13-83191ed4d718> in f(a)
2 def f(a):
3 print(a)
----> 4 print(b)
5 b = 9
6 f(3)
UnboundLocalError: local variable 'b' referenced before assignment
原因:python编译器认为b是局部变量,由于定义b在大约b之后,因此报错。
代码可以改为
b = 6
def f(a):
global b # 指定b是全局b
print(a)
print(b)
b = 9
f(3)
输出:
3
6
2. 闭包
闭包:是一种函数,一种延伸了作用域的函数。具体来说,该函数引入了一个即没有在函数中定义、也不是全局变量的变量。
上面的定义有点绕,来看个例子。
def make_averager(): # 一个返回函数averager的函数
series = []
# 将new_value存储至series中,并计算series的均值
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager()
print(avg(10))
print(avg(11))
print(avg(12))
输出:
10.0
10.5
11.0
函数averager()
引用了其外部函数make_average()
的局部变量series
,但是按照上面代码中的调用方式avg(10)
就应用直接报错,因为随着make_average()
调用的结束,局部变量series
应该会被销毁,导致函数averager()
无法引用到该变量。但是没有报错啊!!!Series并没有被销毁掉!
这就是python中闭包的概念了,函数averager()
就是一个闭包,其引用了一个自由变量(free variable)Series
。
基于上面的例子和概念介绍,给出两个概念的定义:
自由变量(free variable):未在本地作用域绑定的变量。
闭包(closure):一种函数,它会保留定义函数时存在的自由变量绑定,这样调用函数时虽然定义作用域不可用了,但是仍然能使用绑定的自用变量。
3.nonlocal
下面通过一个例子来说明关键字nonlocal的作用。下面的例子仍然是一个计算平均数的函数,但该函数不在保存历史值,而是保存总值和元素个数。
def make_averager():
count = 0
total = 0
def averager(new_value):
'''
首先,count += 1等价于count = count + 1;
其次,count = count + 1说明要给变量count进行赋值,这会将count转换为局部变量(不赋值则不会转换为局部变量);
最后,为了防止将count当做局部变量,因此使用nonlocal关键字;(类似global)
'''
nonlocal count, total
count += 1
total += new_value
return total/count
return averager
avg = make_averager()
avg(10)
avg(5)
输出:
7.5
闭包例子中不需要使用nonlocal的原因:
由于列表示可变对象,因此调用函数append并不会给变量series进行赋值,所以其不会被当成局部变量。
四、一个有意义的例子:使用装饰器来计算函数的执行时间
装饰器的典型行为:把被装饰函数替换成新函数,二者接收相同的参数,在将被装饰函数的值返回以外,同时做些额外的操作。
1. 定义一个普通的函数
import time
def snooze(seconds):
"""打盹"""
time.sleep(seconds)
return "大意了,没有闪!"
print(snooze.__name__)
print(snooze.__doc__)
print(snooze(3))
print(snooze(seconds=3))
输出:
snooze
打盹
大意了,没有闪!
大意了,没有闪!
2. 定义一个计算函数执行时间的装饰器
import time
from datetime import datetime
def clock(func):
def clocked(*args, **kwargs):
"""计时"""
start_time = datetime.now()
result = func(*args, **kwargs)
elapsed = (datetime.now()-start_time).seconds
return result, elapsed # 返回函数的原始结果和执行时间
return clocked
# 使用@clock装饰函数snooze
@clock
def snooze(seconds):
"""打盹"""
time.sleep(seconds)
return "大意了,没有闪!"
print(snooze.__name__)
print(snooze.__doc__)
print(snooze(3))
print(snooze(seconds=3))
输出:
clocked
计时
('大意了,没有闪!', 3)
('大意了,没有闪!', 3)
可以发现:原始函数的__name__和__doc__被替换了,为了不影响原始函数的这两个属性,可以使用装饰器@functools.wraps,如下:
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
"""计时"""
start_time = datetime.now()
result = func(*args, **kwargs)
elapsed = (datetime.now()-start_time).seconds
return result, elapsed
return clocked
@clock
def snooze(seconds):
"""打盹"""
time.sleep(seconds)
return "大意了,没有闪!"
print(snooze.__name__)
print(snooze.__doc__)
print(snooze(3))
print(snooze(seconds=3))
输出:
snooze
打盹
('大意了,没有闪!', 3)
('大意了,没有闪!', 3)
五、叠放装饰器
def wake1(func):
def wakeup(*args, **kwargs):
result = func(*args, **kwargs)
return result + "张三:快别睡了.\n"
return wakeup
def wake2(func):
def wakeup(*args, **kwargs):
result = func(*args, **kwargs)
return result + "李四:起来嗨!"
return wakeup
@wake2
@wake1
def snooze():
return "我:困了,要睡觉了.\n"
print(snooze())
输出:
我:困了,要睡觉了.
张三:快别睡了.
李四:起来嗨!
六、参数化装饰器
装饰器默认会把被装饰函数做为第一个参数传递给装饰器,但是如何为装饰器设定其他参数。那就是使用装饰器工厂函数(也就是在装饰器外在套一层函数)。
def wake_factory(level): # 装饰器工厂函数
def wake_decorator(func): # 装饰器
def wakeup(*args, **kwargs):
result = func(*args, **kwargs)
if level>0:
result += "张三:快别睡了.\n"
if level>1:
result += "李四:起来嗨!"
return result
return wakeup
return wake_decorator
@wake_factory(level=1)
def snooze():
return "我:困了,要睡觉了.\n"
print("level=1\n" + snooze())
@wake_factory(level=2)
def snooze():
return "我:困了,要睡觉了.\n"
print("level=2\n" + snooze())
输出:
level=1
我:困了,要睡觉了.
张三:快别睡了.
level=2
我:困了,要睡觉了.
张三:快别睡了.
李四:起来嗨!
七、装饰器functools.lru_cache
装饰器lru_cache
实现了LRU(Least Recently Used)算法,其能将函数在指定参数下的值存储起来,从而减少函数的执行时间,将存在的值直接返回。
先来看一个斐波那契数列的计算时间
from datetime import datetime
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
start_time = datetime.now()
fibonacci(36)
elapsed = (datetime.now()-start_time).seconds
print(elapsed)
输出:
5
再看使用装饰器lru_cache
后的计算时间
from datetime import datetime
from functools import lru_cache
@lru_cache()
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
start_time = datetime.now()
fibonacci(36)
elapsed = (datetime.now()-start_time).seconds
print(elapsed)
输出:
0
八、装饰器functools.singledispatch
python并不直接支持函数重载,但是可以使用装饰器functools.singleispatch
来实现函数的重载。具体方法如下例:
from functools import singledispatch
@singledispatch
def show(obj):
print(obj, type(obj), "obj")
# 参数为str的重载
@show.register(str)
def _(text):
print(text, type(text), "str")
# 参数为int的重载
@show.register(int)
def _(n):
print(n, type(n), "int")
show("点赞")
show(666)
show(66.6)
输出:
点赞 <class 'str'> str
666 <class 'int'> int
66.6 <class 'float'> obj