高逼格利器之Python闭包与装饰器

来源:互联网 发布:淘宝佣金怎么算 编辑:程序博客网 时间:2024/06/05 10:04

生活在魔都的小明,终于攒够了首付,在魔都郊区买了一套房子;有一天,小明踩了狗屎,中了一注彩票,得到了20w,小明很是欢喜,于是想干脆用这20万来装修房子吧(decoration);

整个装修过程,小明费心费力,终于,装修结束了,小明入住了新家;

可是,住了一段时间,小明发现,白色的墙壁太没有逼格,怎么办呢?他想要重新刷墙(重构原始函数),但是作为程序猿的小明想到,以后总有一天新的墙面要看腻,为什么不在原来的墙面上贴上壁纸呢,选择还比较多;于是他采用了贴壁纸的方案(装饰器);

从此,小明过上了随意更改壁纸的快乐生活;

 

说到装饰器,最好先搞明白闭包,说到闭包的原理,就不得不提到命名空间,关于命名空间,这里不详赘述,可以看我的另一篇关于命名空间的博客;

python命名空间的查找顺序遵循LEGB规则,即:

局部(local)->封闭(closure)->全局(global)->内置(builtin);

L-Local(function);函数内的名字空间,作用范围为当前函数或者类方法;
E-Enclosing function locals;外部嵌套函数的名字空间(例如closure),作用范围为闭包函数;
G-Global(module);函数定义所在模块(文件)的名字空间,作用范围为当前模块;
B-Builtin(Python);Python内置模块的名字空间,作用范围为所有模块;
 
我们要说的闭包就和第二层E息息相关;

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_arg4     return in_func5 6 add_10_counter = out_func(10)7 print(add_10_counter(5))8 print(add_10_counter(8))
View Code

结果为:

1518

让我们来分析一下这个函数:

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_arg4     return in_func5 6 add_10_counter = out_func(10)7 print(add_10_counter.__closure__)8 print(add_10_counter.__closure__[0].cell_contents)
View Code

结果:

(<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()
View Code

目前公司有条不紊的进行着,但是,以前基础平台的开发人员在写代码时候没有关注验证相关的问题,即:基础平台的提供的功能可以被任何人使用。现在需要对基础平台的所有功能进行重构,为平台提供的所有功能添加验证机制,即:执行功能前,先进行验证。

老大把工作交给 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     # 验证111     # 验证212     # 验证313     print 'f2'14  15 def f3():16     # 验证117     # 验证218     # 验证319     print 'f3'20  21 def f4():22     # 验证123     # 验证224     # 验证325     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()
View Code

虽然业务部门不需要做任何改变,但是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'
View Code

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 @w110 def f1():11     print 'f1'12 @w113 def f2():14     print 'f2'15 @w116 def f3():17     print 'f3'18 @w119 def f4():20     print 'f4'
View Code

代码中的@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')
View Code

要求打印购物车商品时,先进行用户信息验证

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)
View Code

 上面代码执行以后使我们想要的结果,但是有个重要问题:我们希望的是通过调用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')
View Code

结果为:

authentication passadding None into shopping carTraceback (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
View Code

因为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 func10 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')
View Code

这样就解决了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_func12 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')
View Code

结果:

authentication passadding apple into shopping car
View Code

正是我们想要的结果!且不存在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_func12 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)
View Code

结果:

authentication passadding apple into shopping cardone

成功解决;

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_func12 13 @login14 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)
View Code

其中@login就是一个装饰器,当代码运行到@login时,解释器会将:

@logindef 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 res14         return hello_with_time15     return wrapper16 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')
View Code

上例中,执行到:

@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 res13         return hello_with_time14     return wrapper15 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 res24         return hello_with_time25     return wrapper26 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')
View Code

上例中,当执行到:

@login_end(end_time)@login_start(start_time)def hello

解释为:

hello = login_end(end_time)(login_start(start_time)(hello))

看上去有点晕,实际上你就认为加了两次功能,第一次加功能使用了装饰器@login_start,第二次加功能用了装饰器@login_end;

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 wrapper10  11 @log_order_event12 def order_pizza(*toppings):13     # let's get some pizza!
View Code

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_summary10 def fetch_customer_data():11     # ...12  13 @validate_summary14 def query_orders(criteria):15     # ...16  17 @validate_summary18 def create_invoice(params):19     # ...
View Code

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)
View Code

这里有一个全局对象 app,此对象有一个 route 方法。此 route 函数返回一个用于修饰请求处理函数的装饰器。这背后的处理是非常复杂的,但是对于使用 Flask 的程序员来说,所有复杂的东西都被隐藏起来了。

 4)复用不能复用的代码

假设有一个古怪的 API。你可以通过 HTTP 发送 JSON 格式的请求,它 99.9% 的情况下都是正确工作的。但是,小部分请求会返回服务器内部错误的结果。这时候,你需要重新发送请求。在这种情况下,你需要实现重试逻辑,像这样:

1 resp = None2 while True:3     resp = make_api_call()4     if resp.status_code == 500 and tries < MAX_TRIES:5         tries += 16         continue7     break8 process_response(resp)
View Code

现在假设你的代码库中有很都地方都进行调用了函数  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 += 113                 continue14             break15         return resp16     return retried_func17  18 This gives you an easy-to-use @retry decorator:19  20 @retry21 def make_api_call():22     # ....
View Code

总结:

装饰器是面向过程编程的利器,学习装饰器是python最有价值的工具之一

原创粉丝点击