参考:
https://blog.csdn.net/bqw18744018044/article/details/113720717
一、基本概念和作用
1. 装饰器
装饰器(decorate)必须是可调用对象(Callable),其参数是一个函数,称为“被装饰函数”,其输出也是一个函数(或者可调用对象)。一句话:装饰器是处理函数的函数。
2.装饰器的功能
装饰器的功能是,把一个函数转换成另一个函数。假设存在一个定义好的装饰器decorate,那么使用该装饰器的方法如下:
@decorate def target(): print('running target()') 上面代码等价于 def target(): print('running target()') target = decorate(target)
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("五连鞭")
输出:
没错!我装饰器已经执行了. 没错!我装饰器已经执行了.
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
闭包:是一种函数,一种延伸了作用域的函数。具体来说,该函数引入了一个即没有在函数中定义、也不是全局变量的变量。
上面的定义有点绕,来看个例子。
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):一种函数,它会保留定义函数时存在的自由变量绑定,这样调用函数时虽然定义作用域不可用了,但是仍然能使用绑定的自用变量。
下面通过一个例子来说明关键字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 打盹 大意了,没有闪! 大意了,没有闪!
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 我:困了,要睡觉了. 张三:快别睡了. 李四:起来嗨!
装饰器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
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