本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理
最近阅读《流畅的python》看见其用函数写装饰器部分写的很好,想写一些自己的读书笔记。众所周知,装饰器是python学习过程中的一道门槛,初学者学习时往往是知其然,不知其所以然,这样的结果是导致一段时间后会遗忘掉该部分内容,只好再次去学习,拉高了学习成本。
想学好python的装饰器,需要明白一下几点;
1:闭包
1)函数嵌套
2)内部函数使用外部函数的变量
3)外部函数的返回值为内部函数
接下来看看《流畅的python》中的例子,我稍微修改了一下:
>>> def make_averager(series=[]): ... def averager(new_value): ... series.append(new_value) ... total = sum(series) ... return total/len(series) ... return averager ... >>> avg = make_averager() >>> avg <function make_averager.<locals>.averager at 0x10b82cb00> >>> avg(10) 10.0 >>> avg(11) 10.5 >>> avg(12) 11.0
函数 make_averager 实现了一个 计算当前所有数字的平均值的功能,不断的添加一个值,然后计算当前的平均值。
avg这个对象内存地址指向了make_averager这个函数的内部函数中,而且avg通过不断的添加值进行平均值计算,按理说在这个内部函数没有存储new_value的空间,而且在make_averager对avg赋值后,函数返回后series这个变量也应该消失了,但是avg却依然可以进行计算。
这就是闭包,内部函数averager使用外面的自由变量,也就是属于make_averager的局部变量series
>>> avg.__code__.co_varnames ('new_value', 'total') >>> avg.__code__.co_freevars ('series',)
可以发现avg的自由变量是make_averager的局部变量,就是说闭包里的内部函数可以使用外部函数的变量,即我们上面提到的第二点:“内部函数使用外部函数的变量”, 注:自由变量只能read,并不能write,不然会提示本地变量并没有赋值的错误,我们举的例子没遇到这个问题,因为我们没有给 series 赋值,我们只是调 用 series.append,并把它传给 sum 和 len。也就是说,我们利用了 列表是可变的对象这一事实 。下图是书中提供的闭包范围图:
2:装饰器的实现
所谓装饰器,就是在不改变基础函数的功能上再次给它封装一层,达到我们想要的目的,接下来我举个简单的例子:
deco_demo.py
1 def col(func): 2 def inner(*args, **kwargs): 3 print(func.__name__) 4 print(locals()) 5 print(inner.__code__.co_varnames) 6 print(inner.__code__.co_freevars) 7 return func(*args, **kwargs) 8 return inner 9 10 11 @col 12 def new_add(x): 13 return x+2 14 15 16 def new_add_1(x): 17 return x+3 18 19 20 print(new_add(3)) 21 22 new_add_1 = col(new_add_1) 23 print(new_add_1(3))
下方是它的返回结果:
new_add {'args': (3,), 'kwargs': {}, 'func': <function new_add at 0x10d32aa70>, 'inner': <function col.<locals>.inner at 0x10d32acb0>} ('args', 'kwargs') ('func', 'inner') 5 new_add_1 {'args': (3,), 'kwargs': {}, 'func': <function new_add_1 at 0x10d32add0>, 'inner': <function col.<locals>.inner at 0x10d32a8c0>} ('args', 'kwargs') ('func', 'inner') 6
1-8:是定义的一个简单装饰器,
3:打印当被装饰函数的名字
4:打印inner这个内部函数中的所有变量
5:打印当前inner的局部变量;
6:则打印自由变量;
11-13:修饰了一个简单函数
16,22,23:@这个语法糖,背后实现的过程;
也就是说 col(new_add) 返回的是当前的内部函数的内存地址,而这个调用这个内部函数时会使用自由变量func即col的局部变量,进而达到装饰器的目的;
有参数的装饰器实现
既然无参数的装饰器即@col ,通过内部函数的方式装饰基础函数,那么我们调用有参数的装饰器 则可以再原本的基础即函数col再封装一层函数,使其达到可以通过装饰器传参数的目的
1 from functools import wraps 2 3 4 def col(string="hello world"): 5 def decorate(func): 6 @wraps(func) 7 def inner(*args, **kwargs): 8 print(string) 9 return func(*args, **kwargs) 10 return inner 11 return decorate 12 13 14 @col() 15 def new_add(x): 16 return x+2 17 18 19 @col("hello python") 20 def new_add_1(x): 21 return x+3 22 23 24 def new_add_2(x): 25 return x+4 26 27 28 print(new_add(1)) 29 print(new_add_1(1)) 30 31 32 new_add_2 = col("hello china")(new_add_2) 33 print(new_add_2(1))
导入wrap是为了修复这个装饰器的名称, new_add.__name__
调用时指向被装饰的函数,而不是内部函数,有兴趣的小伙伴可以去了解一下;
4-11:实现了一个带参数的装饰器,最外层返回的是我们真正的装饰器;
32-33:则是@这个装饰器语法糖背后的实现过程
可以发现new_add与new_add_1这两个函数的装饰器是两个不同值,而我们的装饰器也返回了不同的对应情况
hello world 3 hello python 4 hello china 5
间而言之:装饰器就是在我们需要添加功能的函数上进而封装一层,而python的语法糖@背后,帮助我们省略掉了这些赋值的过程;
3:装饰器何时调用
关于装饰器何时运行,我们分两种情况讨论,一种是当作脚本运行时,另一种是当作模块被导入时;
1 registry = [] 2 3 4 def register(func): 5 print(f"running register {func}") 6 registry.append(func) 7 return func 8 9 10 @register 11 def f1(): 12 print('running f1()') 13 14 15 @register 16 def f2(): 17 print('running f2()') 18 19 20 def f3(): 21 print('running f3()') 22 23 24 def main(): 25 print('running main()') 26 print('regisry ->', registry) 27 f1() 28 f2() 29 f3() 30 31 32 if __name__ == '__main__': 33 main()
当作独立脚本运行时:
running register <function f1 at 0x103f9dcb0> running register <function f2 at 0x103f9ddd0> running main() regisry -> [<function f1 at 0x103f9dcb0>, <function f2 at 0x103f9ddd0>] running f1() running f2() running f3()
被当作模块导入时:
>>> import registration running register <function f1 at 0x1005a2710> running register <function f2 at 0x1005a2b90> >>> registration.registry [<function f1 at 0x1005a2710>, <function f2 at 0x1005a2b90>]
该段代码的装饰器主要功能是:记录了被装饰函数的个数,通常是web框架以这种方式把函数注册到中央注册器的某处。
总结:可以发现装饰器无论是作为模块被导入,还是单独的脚本运行,它都是优先执行的;
4:装饰器的常用模块
之前介绍的function.wraps不用说了,接下来介绍两种神奇的装饰器;
1:singledispatch
何为singledispatch ?
就是在不改变函数本身的功能上复用该函数,达到重复使用函数名的目的,有点类似多态的感觉;可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用@singledispatch 装饰的普通函数会变成泛函数(generic function);根据第一个参数的类型,以不同方式执行相同操作的一组函数
1 from functools import singledispatch 2 3 4 @singledispatch 5 def hello(obj): 6 print(obj) 7 8 9 @hello.register(str) 10 def _(text): 11 print("hello world "+text) 12 13 14 @hello.register(int) 15 def _(n): 16 print(n) 17 18 19 hello({"what": "say"}) 20 print('*'*30) 21 hello('dengxuan') 22 print('*'*30) 23 hello(123)
{'what': 'say'} ****************************** hello world dengxuan ****************************** 123
从该段代码中我们可以发现,当使用singledispatch这个装饰器时,函数hello可以根据不同的参数返回不同的结果。这样的好处就是极大的减少代码中的if/elif/else,并且可以复用函数名称 _ (下横线代表没用),降低了代码的耦合度,达到了多态的效果。
2:lru_cache
根据书上原话:
functools.lru_cache 是非常实用的装饰器,它实现了备忘 (memoization)功能。这是一项优化技术,它把耗时的函数的结果保存 起来,避免传入相同的参数时重复计算。LRU 三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存 条目会被扔掉。
1 from my_tools.runtime import clock 2 import functools 3 4 5 @functools.lru_cache() 6 @clock 7 def fibonacci(n): 8 if n < 2: 9 return n 10 return fibonacci(n-2)+fibonacci(n-1) 11 12 13 if __name__ == '__main__': 14 print(fibonacci(6))
第5行:注释funtools.lru_cache()
返回结果:
[0.00000046] fibonacci(0) -> 0 [0.00000053] fibonacci(1) -> 1 [0.00006782] fibonacci(2) -> 1 [0.00000030] fibonacci(1) -> 1 [0.00000035] fibonacci(0) -> 0 [0.00000037] fibonacci(1) -> 1 [0.00001312] fibonacci(2) -> 1 [0.00002514] fibonacci(3) -> 2 [0.00010535] fibonacci(4) -> 3 [0.00000030] fibonacci(1) -> 1 [0.00000030] fibonacci(0) -> 0 [0.00000037] fibonacci(1) -> 1 [0.00001209] fibonacci(2) -> 1 [0.00002376] fibonacci(3) -> 2 [0.00000028] fibonacci(0) -> 0 [0.00000038] fibonacci(1) -> 1 [0.00001210] fibonacci(2) -> 1 [0.00000028] fibonacci(1) -> 1 [0.00000036] fibonacci(0) -> 0 [0.00000034] fibonacci(1) -> 1 [0.00001281] fibonacci(2) -> 1 [0.00002466] fibonacci(3) -> 2 [0.00004897] fibonacci(4) -> 3 [0.00008414] fibonacci(5) -> 5 [0.00020196] fibonacci(6) -> 8 8
当取消掉第5行注释时;
[0.00000040] fibonacci(0) -> 0 [0.00000049] fibonacci(1) -> 1 [0.00008032] fibonacci(2) -> 1 [0.00000066] fibonacci(3) -> 2 [0.00009398] fibonacci(4) -> 3 [0.00000063] fibonacci(5) -> 5 [0.00010943] fibonacci(6) -> 8 8
可以发现,lru_cache()这个装饰器,极大的提高了计算性能;
maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的 幂。typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。顺 便说一下,因为 lru_cache 使用字典存储结果,而且键根据调用时传 入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的。
5:多重装饰器
@d1 @d2 def f(): print('hello world') ########################### def f(): print("hello world") f = d1(d2(f))
上下两块代码是等效效果;
想要获取更多Python学习资料可以加
QQ:2955637827私聊
或加Q群630390733
大家一起来学习讨论吧!