[Python小记] 通俗的理解闭包 闭包能帮我们做什么?

[Python小记] 通俗的理解闭包 闭包能帮我们做什么?热身首先给出闭包函数的必要条件 闭包函数必须返回一个函数对象 闭包函数返回的那个函数必须引用外部变量 一般不能是全局变量 而返回的那个函数内部不一定要 return 几个典型的闭包例子 ENV gt gt gt Python3 6 NO 1defline conf a b defline x r

热身

首先给出闭包函数的必要条件:

  • 闭包函数必须返回一个函数对象
  • 闭包函数返回的那个函数必须引用外部变量(一般不能是全局变量),而返回的那个函数内部不一定要return

几个典型的闭包例子:

# ENV>>> Python 3.6 # NO.1 def line_conf(a, b): def line(x): return a * x + b return line # NO.2 def line_conf(): a = 1 b = 2 def line(x): print(a * x + b) return line # NO.3 def _line_(a, b): def line_c(c): def line(x): return a * (x 2) + b * x + c return line return line_c

正文

    通过前面的例子相信你能够初步理解闭包的三个必要条件了。一脸懵逼?没关系,下面从python中函数的作用域开始讲起,一步步地的理解闭包。

    一、函数中的作用域

           Python中函数的作用域由def关键字界定,函数内的代码访问变量的方式是从其所在层级由内向外的,如“热身”中的第一段代码:

def line_conf(a, b): def line(x): return a * x + b return line

           嵌套函数line中的代码访问了a和b变量,line本身函数体内并不存在这两个变量,所以会逐级向外查找,往上走一层就找到了来自主函数line_conf传递的a, b。若往外直至全局作用域都查找不到的话代码会抛异常。

            注意:不管主函数line_conf下面嵌套了多少个函数,这些函数都在其作用域内,都可以在line_conf作用域内被调用。

            思考上面这段代码实现了什么功能?

# 定义两条直线 line_A = line_conf(2, 1) # y=2x+b line_B = line_conf(3, 2) # y=3x+2 # 打印x对应y的值 print(line_A(1)) # 3 print(line_B(1)) # 5

            是否感觉“哎哟,有点意思~”,更有意思的在后面呢。

            现在不使用闭包,看看需要多少行代码实现这个功能:

def line_A(x): return 2 * x + 1 def line_B(x): return 3 * x + 2 print(line_A(1)) # 3 print(line_B(1)) # 5 

            不包括print语句的代码是4行,闭包写法是6行,看起来有点不对劲啊?怎么闭包实现需要的代码量还多呢?别急,我现在有个需求:

            再定义100条直线!

            那么现在谁的代码量更少呢?很明显这个是可以简单计算出来的,采用闭包的方式添加一条直线只需要加一行代码,而普通做法需要添两行代码,定义100条直线两种做法的代码量差为:100+6 -(100*2+4) = -98。需要注意的是,实际环境中定义的单个函数的代码量多达几十上百行,这时候闭包的作用就显现出来了,没错,大大提高了代码的可复用性!

            注意:闭包函数引用的外部变量不一定就是其父函数的参数,也可以是父函数作用域内的任意变量,如“热身”中的第二段代码:

def line_conf(): a = 1 b = 2 def line(x): print(a * x + b) return line

    二、如何显式地查看“闭包”

            接上面的代码块:

L = line_conf() print(line_conf().__closure__) #(<cell at 0x05BE3530: int object at 0x1DA2D1D0>, # <cell at 0x05C4DDD0: int object at 0x1DA2D1E0>) for i in line_conf().__closure__: #打印引用的外部变量值 print(i.cell_contents) #1 ; #2

            __closure__属性返回的是一个元组对象,包含了闭包引用的外部变量。

          ·  若主函数内的闭包不引用外部变量,就不存在闭包,主函数的_closure__属性永远为None:

def line_conf(): a = 1 b = 2 def line(x): print(x + 1) # <<<------ return line L = line_conf() print(line_conf().__closure__) # None for i in line_conf().__closure__: # 抛出异常 print(i.cell_contents)

           ·  若主函数没有return子函数,就不存在闭包,主函数不存在_closure__属性:

def line_conf(): a = 1 b = 2 def line(x): print(a * x + b) return a + b # <<<------ L = line_conf() print(line_conf().__closure__) # 抛出异常   

    三、为何叫闭包?

            先看代码:

def line_conf(a): b = 1 def line(x): return a * x + b return line line_A = line_conf(2) b = 20 print(line_A(1)) # 3

              如你所见,line_A对象作为line_conf返回的闭包对象,它引用了line_conf下的变量b=1,在print时,全局作用域下定义了新的b变量指向20,最终结果仍然引用的line_conf内的b。这是因为,闭包作为对象被返回时,它的引用变量就已经确定(已经保存在它的__closure__属性中),不会再被修改。

               是的,闭包在被返回时,它的所有变量就已经固定,形成了一个封闭的对象,这个对象包含了其引用的所有外部、内部变量和表达式。当然,闭包的参数例外。

    四、闭包可以保存运行环境

               思考下面的代码会输出什么?

_list = [] for i in range(3): def func(a): return i+a _list.append(func) for f in _list: print(f(1))
_list = [] for i in range(3): def func(i): def f_closure(a): # <<<--- return i + a return f_closure _list.append(func(i)) # <<<--- for f in _list: print(f(1))

          关于这个问题的深入探讨(python新手理解起来可能需要点时间),我们先看下面的代码(2019/5/19增):

_list = [] for i in range(3): def func(): return i+1 func.__doc__ = i func.__hash__ = i func.__repr__ = i func.__defaults__ = tuple([i]) #这个属性必须是tuple类型 func.__name__ = f'{i}' func.hello = i #自定义一个属性并赋值 # 不能再玩了 _list.append(func) for f in _list: print(f.__doc__, f.__hash__, f.__repr__, f.__defaults__, f.__name__, f.hello, f(), ) # 输出 # 0 0 0 (0,) 0 0 3 # 1 1 1 (1,) 1 1 3 # 2 2 2 (2,) 2 2 3

    代码中我在保存函数时,修改了函数的一些属性(前几个叫做magic method,是函数对象默认拥有的),使它们等于循环内的变量i,hello属性显然是我自定义的一个属性,也让它等于了i。

    然后,我们循环打印每个函数的这些属性,可以发现,咦~  这些属性居然可以保存这个变量i     :)

    嗯,是的,函数的一些基本属性在定义时就会有一个初始的确定值(不论这个值是由可变或不可变对象构成,都是一个完整拷贝,不受源变量变动影响); 闭包保存这个变量的原理是一样的,它用的是函数的__closure__属性,这个属性还有一点特殊,它是只读的,不能由人为修改。(function还有一个__code__属性,这个对象很牛)

    这部分内容是对闭包和函数对象的更深一层的探讨,理解后更上一层楼;

    不过当你不知道这些属性时是做什么用时,最好不要修改它们。

五、闭包的实际应用

                现在你已经逐渐领悟“闭包”了,趁热打铁,再来一个小例子:

def who(name): def do(what): print(name, 'say:', what) return do lucy = who('lucy') john = who('john') lucy('i want drink!') lucy('i want eat !') lucy('i want play !') john('i want play basketball') john('i want to sleep with U,do U?') lucy("you just like a fool, but i got you!")

                看到这里,你也可以试着自己写出一个简单的闭包函数。

                OK,现在来看一个真正在实际环境中会用到的案例:

                1、【闭包实现快速给不同项目记录日志】

import logging def log_header(logger_name): logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(name)s] %(levelname)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') logger = logging.getLogger(logger_name) def _logging(something,level): if level == 'debug': logger.debug(something) elif level == 'warning': logger.warning(something) elif level == 'error': logger.error(something) else: raise Exception("I dont know what you want to do?" ) return _logging project_1_logging = log_header('project_1') project_2_logging = log_header('project_2') def project_1(): #do something project_1_logging('this is a debug info','debug') #do something project_1_logging('this is a warning info','warning') # do something project_1_logging('this is a error info','error') def project_2(): # do something project_2_logging('this is a debug info','debug') # do something project_2_logging('this is a warning info','warning') # do something project_2_logging('this is a critical info','error') project_1() project_2()
#输出 2018-05-26 22:56:23 [project_1] DEBUG  this is a debug info 2018-05-26 22:56:23 [project_1] WARNING  this is a warning info 2018-05-26 22:56:23 [project_1] ERROR  this is a error info 2018-05-26 22:56:23 [project_2] DEBUG  this is a debug info 2018-05-26 22:56:23 [project_2] WARNING  this is a warning info 2018-05-26 22:56:23 [project_2] ERROR  this is a critical info 

                这段代码实现了给不同项目logging的功能,只需在你想要logging的位置添加一行代码即可。

扩展: python中的使用@语法实现的单例模式就是利用闭包实现的,只不过用了@作为语法糖,使写法更简洁,闭包函数将函数的唯一实例保存在它内部的__closure__属性中,在再次创建函数实例时,闭包检查该函数实例已存在自己的属性中,不会再让他创建新的实例,而是将现有的实例返给它。

声明:本文章为个人对技术的理解与总结,不能保证毫无瑕疵,接受网友的斧正。

    

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/231956.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号