高逼格利器之Python闭包与装饰器
生活在魔都的小明,终于攒够了首付,在魔都郊区买了一套房子;有一天,小明踩了狗屎,中了一注彩票,得到了20w,小明很是欢喜,于是想干脆用这20万来装修房子吧(decoration);
整个装修过程,小明费心费力,终于,装修结束了,小明入住了新家;
可是,住了一段时间,小明发现,白色的墙壁太没有逼格,怎么办呢?他想要重新刷墙(重构原始函数),但是作为程序猿的小明想到,以后总有一天新的墙面要看腻,为什么不在原来的墙面上贴上壁纸呢,选择还比较多;于是他采用了贴壁纸的方案(装饰器);
从此,小明过上了随意更改壁纸的快乐生活;
说到装饰器,最好先搞明白闭包,说到闭包的原理,就不得不提到命名空间,关于命名空间,这里不详赘述,可以看我的另一篇关于命名空间的博客;
python命名空间的查找顺序遵循LEGB规则,即:
局部(local)->封闭(closure)->全局(global)->内置(builtin);
E-Enclosing function locals;外部嵌套函数的名字空间(例如closure),作用范围为闭包函数;
G-Global(module);函数定义所在模块(文件)的名字空间,作用范围为当前模块;
B-Builtin(Python);Python内置模块的名字空间,作用范围为所有模块;
1.闭包
1.1 定义
首先让我们看一下维基上关于闭包的定义:
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
<Python核心编程>里讲到:
如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。定义在外部函数内的但由内部函数引用或者使用的变量称为自由变量。
闭包将内部函数自己的代码和作用域以及外部函数的作用域结合起来。闭包的语法变量不属于全局名称空间或者局部的---而属于其他的名称空间,带着“流浪”的作用域(注意这不同于对象,因为那些变量存货在一个对象的名称空间;但是闭包变量存活在一个函数的名称空间和作用域)。
看了上面的文字是不是有些头昏,等你理解了闭包,回头来看,你就会觉得总结的很好;
看不懂,没关系,让我们通过一个例子来个初体验:
1.2 闭包小例子
1 def out_func(out_arg): 2 def in_func(in_arg): 3 return out_arg+in_arg 4 return in_func 5 6 add_10_counter = out_func(10) 7 print(add_10_counter(5)) 8 print(add_10_counter(8))
结果为:
15 18
让我们来分析一下这个函数:
1)功能很简单,创建了一个带有初始值10的加法器,通过提供一个数字,得到与10相加的结果;
2)功能是由一个out_func函数包裹了一个in_func函数实现的;当执行了add_10_counter = out_func(10)后,out_func(10)返回了一个函数对象in_func给add_10_counter,那么下次调用add_10_counter(x)时,实际上就是在调用in_func(x);但是我们看到in_func使用了变量out_arg,但是问题来了:out_arg即不在局部命名空间,也不在全局命名空间;为什么in_func可以使用out_arg呢?
根据闭包的定义,“如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。定义在外部函数内的但由内部函数引用或者使用的变量称为自由变量。”这样看,in_func就是一个闭包函数,而out_arg就是那个自由变量;而且我们注意到外部函数out_func调用已经结束了,相应的内存空间也已经销毁了,但是自由变量out_arg却没有销毁回收,生命周期得到了延长,为什么呢?这里就涉及到函数的__closure__属性了;
1.3 __closure__属性
在Python中,函数对象有一个__closure__属性,自由变量就被保存在函数对象的__closure__属性中
我们来验证这一点:
1 def out_func(out_arg): 2 def in_func(in_arg): 3 return out_arg+in_arg 4 return in_func 5 6 add_10_counter = out_func(10) 7 print(add_10_counter.__closure__) 8 print(add_10_counter.__closure__[0].cell_contents)
结果:
(<cell at 0x0000000002589618: int object at 0x000000001E32EAF0>,)
10
看到了自由变量out_arg的值10;
这下明白了吧!当外部函数int_func被调用后,自由变量out_arg就会被保存在函数add_10_counter的__closure__属性中,所以add_10_counter可以使用该自由变量;
总结:
在Python中创建一个闭包可以归结为以下三点:
1)闭包函数必须被包裹在另一个函数中
2)闭包函数必须引用了上一级namespace中的变量
3)外部函数必须返回了该闭包函数
上面关于闭包的介绍是为了介绍更重要的装饰器,下面就让我们来认识装饰器
2. 装饰器
网上扒下来的一段,用作引子刚刚好:
2.1 一个有趣的引子
初创公司有N个业务部门,1个基础平台部门,基础平台负责提供底层的功能,如:数据库操作、redis调用、监控API等功能。业务部门使用基础功能时,只需调用基础平台提供的功能即可。如下:
1 ############### 基础平台提供的功能如下 ############### 2 3 def f1(): 4 print 'f1' 5 6 def f2(): 7 print 'f2' 8 9 def f3(): 10 print 'f3' 11 12 def f4(): 13 print 'f4' 14 15 ############### 业务部门A 调用基础平台提供的功能 ############### 16 17 f1() 18 f2() 19 f3() 20 f4() 21 22 ############### 业务部门B 调用基础平台提供的功能 ############### 23 24 f1() 25 f2() 26 f3() 27 f4()
目前公司有条不紊的进行着,但是,以前基础平台的开发人员在写代码时候没有关注验证相关的问题,即:基础平台的提供的功能可以被任何人使用。现在需要对基础平台的所有功能进行重构,为平台提供的所有功能添加验证机制,即:执行功能前,先进行验证。
老大把工作交给 Low B,他是这么做的:
1)做法1
跟每个业务部门交涉,每个业务部门自己写代码,调用基础平台的功能之前先验证。诶,这样一来基础平台就不需要做任何修改了。
当天Low B 被开除了…
老大把工作交给 Low BB,他是这么做的:
2)做法2
1 ############### 基础平台提供的功能如下 ############### 2 3 def f1(): 4 # 验证1 5 # 验证2 6 # 验证3 7 print 'f1' 8 9 def f2(): 10 # 验证1 11 # 验证2 12 # 验证3 13 print 'f2' 14 15 def f3(): 16 # 验证1 17 # 验证2 18 # 验证3 19 print 'f3' 20 21 def f4(): 22 # 验证1 23 # 验证2 24 # 验证3 25 print 'f4' 26 27 ############### 业务部门不变 ############### 28 ### 业务部门A 调用基础平台提供的功能### 29 30 f1() 31 f2() 32 f3() 33 f4() 34 35 ### 业务部门B 调用基础平台提供的功能 ### 36 37 f1() 38 f2() 39 f3() 40 f4()
虽然业务部门不需要做任何改变,但是BB在每个基础平台函数都加入了相同的代码,没有考虑到代码复用,耦合,太低级了;
过了一周 Low BB 被开除了
老大把工作交给 Low BBB,他是这么做的:
3)做法3
只对基础平台的代码进行重构,其他业务部门无需做任何修改
1 ############### 基础平台提供的功能如下 ############### 2 3 def check_login(): 4 # 验证1 5 # 验证2 6 # 验证3 7 pass 8 9 10 def f1(): 11 12 check_login() 13 14 print 'f1' 15 16 def f2(): 17 18 check_login() 19 20 print 'f2' 21 22 def f3(): 23 24 check_login() 25 26 print 'f3' 27 28 def f4(): 29 30 check_login() 31 32 print 'f4'
BBB的想法和BB的是一样的,但是BBB考虑到了代码复用的问题;老大看了下Low BBB 的实现,嘴角漏出了一丝的欣慰的笑,语重心长的跟Low BBB聊了个天:
老大说:
写代码要遵循开放封闭原则,虽然在这个原则是用的面向对象开发,但是也适用于函数式编程,简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展,即:
- 封闭:已实现的功能代码块
- 开放:对扩展开发
如果将开放封闭原则应用在上述需求中,那么就不允许在函数 f1 、f2、f3、f4的内部进行修改代码,老板就给了Low BBB一个实现方案:
4)最终方法
1 def w1(func): 2 def inner(): 3 # 验证1 4 # 验证2 5 # 验证3 6 func() 7 return inner 8 9 @w1 10 def f1(): 11 print 'f1' 12 @w1 13 def f2(): 14 print 'f2' 15 @w1 16 def f3(): 17 print 'f3' 18 @w1 19 def f4(): 20 print 'f4'
代码中的@w1就是一个装饰器;既没有对原基础平台函数f1(),f2(),f3(),f4()代码块进行修改,且实现了需求;
段子讲完了,让我们看看到底装饰器是什么,装饰器到底怎么实现上面功能的?
2.2 装饰器原理
装饰器是Python语言中的高级语法,是个语法糖。主要的功能是对一个函数、方法、或者类进行加工,作用是为已经存在的对象添加额外的功能,提升代码的可读性。
装饰器是设计模式的一种,被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。
现在有一个打印购物车商品的功能
1 def shopping_car(item=None): 2 print('adding %s into shopping car'%item) 3 4 shopping_car('apple')
要求打印购物车商品时,先进行用户信息验证
1)version1.0:
1 '''version1.0''' 2 3 #增加验证功能 4 def login(func): 5 '''这里省略验证用户代码''' 6 #打印验证成功消息 7 print('authentication pass') 8 #同时实现func函数功能 9 func() 10 11 def shopping_car(item=None): 12 print('adding %s into shopping car'%item) 13 14 #实现验证用户,然后打印购物车商品 15 shopping_car = login(shopping_car)
上面代码执行以后使我们想要的结果,但是有个重要问题:我们希望的是通过调用shopping_car('apple')来实现打印购物车商品的功能;
加入shopping_car('apple'),来执行以下代码
1 '''version1.0''' 2 3 #增加验证功能 4 def login(func): 5 '''这里省略验证用户代码''' 6 #打印验证成功消息 7 print('authentication pass') 8 #同时实现func函数功能 9 func() 10 11 def shopping_car(item=None): 12 print('adding %s into shopping car'%item) 13 14 #实现验证用户,然后打印购物车商品 15 shopping_car = login(shopping_car) 16 shopping_car('apple')
结果为:
authentication pass adding None into shopping car Traceback (most recent call last): File "C:/Users/z218680/Desktop/Python/cnblogs/闭包和装饰器.py", line 21, in <module> shopping_car('apple') TypeError: 'NoneType' object is not callable
因为shopping_car得到的是login(shopping_car)的返回结果None,不是一个函数对象,不能调用;那我们就改一下,让代码返回一个函数对象;
2)version1.1
1 '''version1.1''' 2 3 #增加验证功能 4 def login(func): 5 '''这里省略验证用户代码''' 6 #打印验证成功消息 7 print('authentication pass') 8 #同时实现func函数功能 9 return func 10 11 def shopping_car(item=None): 12 print('adding %s into shopping car'%item) 13 14 #实现验证用户,然后打印购物车商品 15 shopping_car = login(shopping_car) 16 shopping_car('apple')
这样就解决了version1.0的问题,并且实现了功能;但是好像有问题,如果我们把shopping_car('apple')这句注释掉,运行一下呢
结果:
authentication pass
即使不执行shopping_car('apple'),用户验证代码也会执行,因为用户验证在shopping_car = login(shopping_car)这一步就执行了,这显而不是我们希望的;因为:
a)我们希望的是在执行shopping_car('apple')以后再进行用户验证
b)假如shopping_car('apple')是一个业务部门调用的API,如果代码如上,业务部门调用的时候根本就不会去执行shopping_car = login(shopping_car),所以还是不会进行用户验证;又回到了原点,等于没做
基于此,我们再修改一下代码
3)version1.2
1 '''version1.2''' 2 3 #增加验证功能 4 def login(func): 5 def inner_func(item): 6 '''这里省略验证用户代码''' 7 #打印验证成功消息 8 print('authentication pass') 9 #同时实现func函数功能 10 func(item) 11 return inner_func 12 13 def shopping_car(item=None): 14 print('adding %s into shopping car'%item) 15 16 #实现验证用户,然后打印购物车商品 17 shopping_car = login(shopping_car) 18 shopping_car('apple')
结果:
authentication pass adding apple into shopping car
正是我们想要的结果!且不存在version1.1中的问题
仔细看一下这段代码,这不就是一个闭包吗!只是自由变量是函数对象
但是,上面的代码还是有问题,问题是如果shopping_car是一个有返回值功能呢?再改一下代码
4)version1.3
1 '''version1.3--针对返回值情况''' 2 3 #增加验证功能 4 def login(func): 5 def inner_func(item): 6 '''这里省略验证用户代码''' 7 #打印验证成功消息 8 print('authentication pass') 9 #同时实现func函数功能 10 return func(item) 11 return inner_func 12 13 def shopping_car(item=None): 14 print('adding %s into shopping car'%item) 15 return 'done' 16 17 #实现验证用户,然后打印购物车商品 18 shopping_car = login(shopping_car) 19 res = shopping_car('apple') 20 print(res)
结果:
authentication pass adding apple into shopping car done
成功解决;
python的装饰器其实就是version1.3的代码,只是样子变了下,变成了这样:
4)version1.4
1 '''version1.4--装饰器实现''' 2 3 #增加验证功能 4 def login(func): 5 def inner_func(item): 6 '''这里省略验证用户代码''' 7 #打印验证成功消息 8 print('authentication pass') 9 #同时实现func函数功能 10 return func(item) 11 return inner_func 12 13 @login 14 def shopping_car(item=None): 15 print('adding %s into shopping car'%item) 16 return 'done' 17 18 #实现验证用户,然后打印购物车商品 19 #shopping_car = login(shopping_car) 20 res = shopping_car('apple') 21 print(res)
其中@login就是一个装饰器,当代码运行到@login时,解释器会将:
@login def shopping_car
解释成:
shopping_car = login(shopping_car)
到此为止,装饰器的基本原理就结束了;再优化一下的话,形参可以采用变长参数集*args或者**kwargs;这里不举例子了;
另外,装饰器还有一些高级应用
2.3 装饰器高级应用
1)带参数的装饰器
1 '''带多参数的装饰器 ''' 2 3 from time import ctime,time 4 5 def login(start_time_fuc, end_time_func): 6 def wrapper(f): 7 def hello_with_time(*args, **kwargs): 8 #记录开始时间 9 start_time_fuc() 10 res = f(*args, **kwargs) 11 #记录结束时间 12 end_time_func() 13 return res 14 return hello_with_time 15 return wrapper 16 17 #记录执行的开始时间的函数 18 def start_time(): 19 start = ctime(time()) 20 print('start time: ', start) 21 22 #记录执行的结束时间的函数 23 def end_time(): 24 end = ctime(time()) 25 print('end time: ', end) 26 27 #带参数的装饰器,而且参数还是函数对象 28 @login(start_time, end_time) 29 def hello(name): 30 print('hello', name) 31 32 #调用hello函数 33 hello('world')
上例中,执行到:
@login(start_time, end_time) def hello
解释器解释成:
hello = login(start_time, end_time)(hello)
执行了两个函数,相当于
temp_f = login(start_time, end_time)
hello =temp_f(hello)
注意不同参数对应的不同的位置;
2)多个装饰器
1 '''多个装饰器''' 2 3 from time import ctime,time 4 5 #增加记录开始时间的功能 6 def login_start(start_time_fuc): 7 def wrapper(f): 8 def hello_with_time(*args, **kwargs): 9 #记录开始时间 10 start_time_fuc() 11 res = f(*args, **kwargs) 12 return res 13 return hello_with_time 14 return wrapper 15 16 #增加记录结束时间的功能 17 def login_end(end_time_fuc): 18 def wrapper(f): 19 def hello_with_time(*args, **kwargs): 20 res = f(*args, **kwargs) 21 #记录结束时间 22 end_time_fuc() 23 return res 24 return hello_with_time 25 return wrapper 26 27 #记录执行的开始时间的函数 28 def start_time(): 29 start = ctime(time()) 30 print('start time: ', start) 31 32 #记录执行的结束时间的函数 33 def end_time(): 34 end = ctime(time()) 35 print('end time: ', end) 36 37 #多个装饰器,第一次增加记录开始时间的功能,第二次增加记录结束时间的功能 38 @login_end(end_time) 39 @login_start(start_time) 40 def hello(name): 41 print('hello', name) 42 43 #调用hello函数 44 hello('world')
上例中,当执行到:
@login_end(end_time) @login_start(start_time) def hello
解释为:
hello = login_end(end_time)(login_start(start_time)(hello))
看上去有点晕,实际上你就认为加了两次功能,第一次加功能使用了装饰器@login_start,第二次加功能用了装饰器@login_end;
相当于@login_end是对login_start里的def hello_with_time函数做了装饰(仅仅为了方便理解)
2.4 日常常见应用
1)分析、日志与手段
对于大型应用, 我们常常需要记录应用的状态,以及测量不同活动的数量。通过将这些特别的事件包装到函数或方法中,装饰器可以很轻松地满足这些需求,同时保证代码的可读性。
1 from myapp.log import logger 2 3 def log_order_event(func): 4 def wrapper(*args, **kwargs): 5 logger.info("Ordering: %s", func.__name__) 6 order = func(*args, **kwargs) 7 logger.debug("Order result: %s", order.result) 8 return order 9 return wrapper 10 11 @log_order_event 12 def order_pizza(*toppings): 13 # let's get some pizza!
2)验证以及运行时检查
Python 是一种强类型语言,但是变量的类型却是动态变化的。虽然这会带来很多好处,但是同时这也意味着更容易引入 bug。对于静态语言,例如 Java, 这些 bug 在编译阶段就可以被发现。因而,你可能希望在对传入或返回的数据进行一些自定义的的检查。装饰器就可以让你非常容易地实现这个需求,并一次性将其应用到多个函数上。
想像一下:你有许多函数,每个函数返回一个字典类型,该字典包含一个“summary ”域。这个域的值不能超过 80 个字符的长度。如果违反这个要求,那就是一个错误。下面这个装饰器会在错误发生时抛出 ValueError 异常;
1 def validate_summary(func): 2 def wrapper(*args, **kwargs): 3 data = func(*args, **kwargs) 4 if len(data["summary"]) > 80: 5 raise ValueError("Summary too long") 6 return data 7 return wrapper 8 9 @validate_summary 10 def fetch_customer_data(): 11 # ... 12 13 @validate_summary 14 def query_orders(criteria): 15 # ... 16 17 @validate_summary 18 def create_invoice(params): 19 # ...
3)创建框架
一旦你掌握了如何写装饰器,你就能够从其使用的简单的语法中获益颇丰,你可以为语言添加新的语义使其使用更加简单。接下来最棒的就是你可以自己扩展 Python 语法。
事实上,很多开源框架都是使用的这样的方式。 Web 应用框架 Flask 就是使用装饰器将不同 URL 路由到不同处理 HTTP 请求函数的:
1 # For a RESTful todo-list API. 2 @app.route("/tasks/", methods=["GET"]) 3 def get_all_tasks(): 4 tasks = app.store.get_all_tasks() 5 return make_response(json.dumps(tasks), 200) 6 7 @app.route("/tasks/", methods=["POST"]) 8 def create_task(): 9 payload = request.get_json(force=True) 10 task_id = app.store.create_task( 11 summary = payload["summary"], 12 description = payload["description"], 13 ) 14 task_info = {"id": task_id} 15 return make_response(json.dumps(task_info), 201) 16 17 @app.route("/tasks/<int:task_id>/") 18 def task_details(task_id): 19 task_info = app.store.task_details(task_id) 20 if task_info is None: 21 return make_response("", 404) 22 return json.dumps(task_info)
这里有一个全局对象 app,此对象有一个 route 方法。此 route 函数返回一个用于修饰请求处理函数的装饰器。这背后的处理是非常复杂的,但是对于使用 Flask 的程序员来说,所有复杂的东西都被隐藏起来了。
4)复用不能复用的代码
假设有一个古怪的 API。你可以通过 HTTP 发送 JSON 格式的请求,它 99.9% 的情况下都是正确工作的。但是,小部分请求会返回服务器内部错误的结果。这时候,你需要重新发送请求。在这种情况下,你需要实现重试逻辑,像这样:
1 resp = None 2 while True: 3 resp = make_api_call() 4 if resp.status_code == 500 and tries < MAX_TRIES: 5 tries += 1 6 continue 7 break 8 process_response(resp)
现在假设你的代码库中有很都地方都进行调用了函数 make_api_call,那么是不是需要在每个调用的地方都实现这个 loop 循环呢?是不是每次添加一次调用都要实现一遍这个循环呢?这种模式能难有一个样板代码,除非你使用装饰器,那么这就变得非常简单了:
1 # The decorated function returns a Response object, 2 # which has a status_code attribute. 200 means 3 # success; 500 indicates a server-side error. 4 5 def retry(func): 6 def retried_func(*args, **kwargs): 7 MAX_TRIES = 3 8 tries = 0 9 while True: 10 resp = func(*args, **kwargs) 11 if resp.status_code == 500 and tries < MAX_TRIES: 12 tries += 1 13 continue 14 break 15 return resp 16 return retried_func 17 18 This gives you an easy-to-use @retry decorator: 19 20 @retry 21 def make_api_call(): 22 # ....
总结:
装饰器是面向过程编程的利器,学习装饰器是python最有价值的工具之一
3. 补充
1)从求斐波那契数列引发的一个小问题
a)采用递归方法求斐波那契数列
def fib(i): if i < 2: return i return fib(i-1)+fib(i-2)
运行速度还可以的,但是当我们求fib(100)时,由于递归太深,代码运行时间很久,甚至超时(急性子,等不了)
b)采用装饰器,扩展一下该算法
1 def memo(func): 2 cache = {} 3 def wrap(args): 4 if args not in cache: 5 cache[args] = func(args) 6 return cache[args] 7 return wrap 8 9 @memo 10 def fib(i): 11 if i < 2: return i 12 return fib(i-1)+fib(i-2)
有个疑问,该代码在第5行,cache[args] = func(args)不是还是要去执行原来的fib(i)函数,然后继续递归调用fib(n-1),fib(n-2)吗!通过断言,发现其实不是这样的,根据变量访问LEGB原则,当执行到第12句,调用fib(i-1)时,这时,fib对象是已经经过装饰器装饰过了的fib,实际上是wrap闭包函数的一个引用;
通过id值来验证一下:
def memo(func): cache = {} def wrap(args): print('local fib id in wrap function: ', id(fib)) print('local func id in wrap function: ', id(func)) if args not in cache: cache[args] = func(args) return cache[args] return wrap @memo def fib(i): print('local fib id in fib function ', id(fib)) if i < 2: return i return fib(i-1)+fib(i-2) print('global fib id: ', id(fib)) print(fib(1))
结果:
global fib id: 41411448 local fib id in wrap function: 41411448 local func id in wrap function: 41411312 local fib id in fib function 41411448 1
事实证明,确实如此,但是装饰过的fib不也是通过递归在求值吗?为什么那么快呢?和未装饰的递归有什么区别呢?
还真有,因为原来的递归,进行了重复求值,比如说fib(100)=fib(99)+fib(98),首先需要计算fib(99)=fib(98)+fib(97),这样fib(98)就被计算了两次;而装饰了以后的fib,因为引入了备忘录cache={},每一次的值都会被记录在cache里,所以就不会重复计算;比如fib(100)=fib(99)+fib(98);计算fib(99)=fib(98)+fib(97),执行以后cache就有了记录{98:xxx, 97: yyy},这时回去计算fib(100)=fib(99)+fib(98)时,fib(98)就不需要重复计算了;这就是动态规划