函数进阶(闭包)

闭包

  • 闭包函数的含义:1.内部函数引用外部函数变量      2.从内部函数返回一个值到全局
  • 简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包。实际上闭包可以看做一种更加广义的函数概念。因为其已经不再是传统意义上定义的函数。

一、闭包函数小例子:

def outer_func():
    loc_list = []
    def inner_func(name):
        loc_list.append(len(loc_list) + 1)
        print('%s loc_list = %s' %(name, loc_list))
    return inner_func
clo_func_0 = outer_func()
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_1 = outer_func()
clo_func_1('clo_func_1')
clo_func_1('clo_func_1')

程序运行结果: 

clo_func_0 loc_list = [1]
clo_func_0 loc_list = [1, 2]
clo_func_0 loc_list = [1, 2, 3]
clo_func_0 loc_list = [1, 2, 3, 4]
clo_func_1 loc_list = [1]
clo_func_1 loc_list = [1, 2]

  这是一个闭包函数,但是引用的变量是个列表型,也就是说这个变量是一个可变类型,每次调用函数,其结果都会变化。

同下例的简单函数:

#函数的魔性写法
def func(lst=[]):
    lst.append(1)
    print(lst)

# 默认参数尽量避免使用可变的类型
func()
func()
func()
func()

#调用结果
[1]
[1, 1]
[1, 1, 1]
[1, 1, 1, 1]

 

在闭包函数例子中我们至少可以对闭包中引用的自由变量有如下的认识:

  • 闭包中的引用的自由变量只和具体的闭包有关联,闭包的每个实例引用的自由变量互不干扰。
  • 一个闭包实例对其自由变量的修改会被传递到下一次该闭包实例的调用。

由于这个概念理解起来并不是那么的直观,因此使用的时候很容易掉进陷阱。

二、闭包陷阱

闭包函数陷阱:

def my_func(*args):
    fs = []
    for i in range(3):
        def func():
            return i * i
        fs.append(func)
    return fs

fs1, fs2, fs3 = my_func()
print (fs1())
print (fs2())
print (fs3())

#运行结果
4
4
4

  上面这段代码可谓是典型的错误使用闭包的例子。程序的结果并不是我们想象的结果0,1,4。实际结果全部是4。

这个例子中,my_func返回的并不是一个闭包函数,而是一个包含三个闭包函数的一个list。这个例子中比较特殊的地方就是返回的所有闭包函数均引用父函数中定义的同一个自由变量。

  但这里的问题是为什么for循环中的变量变化会影响到所有的闭包函数?尤其是我们上面刚刚介绍的例子中明明说明了同一闭包的不同实例中引用的自由变量互相没有影响的。而且这个观点也绝对的正确。

  那么问题到底出在哪里?应该怎样正确的分析这个错误的根源。

  其实问题的关键就在于在返回闭包列表fs之前for循环的变量的值已经发生改变了,而且这个改变会影响到所有引用它的内部定义的函数。因为在函数my_func返回前其内部定义的函数并不是闭包函数,只是一个内部定义的函数。

  当然这个内部函数引用的父函数中定义的变量也不是自由变量,而只是当前block中的一个local variable。

def my_func(*args):
    fs = []
    j = 0
    for i in range(3):
        def func():
            return j * j
        fs.append(func)
    j = 2
    return fs
f = my_func()

print(f)

#运行结果:
[<function my_func.<locals>.func at 0x00000000021107B8>, <function my_func.<locals>.func at 0x0000000002110840>,
<function my_func.<locals>.func at 0x00000000021108C8>]

  

  上面的这段代码逻辑上与之前的例子是等价的。这里或许更好理解一点,因为在内部定义的函数func实际执行前,对局部变量j的任何改变均会影响到函数func的运行结果。

  函数my_func一旦返回,那么内部定义的函数func便是一个闭包,其中引用的变量j成为一个只和具体闭包相关的自由变量。后面会分析,这个自由变量存放在Cell对象中。

  使用lambda(兰布达)表达式重写这个例子:

def my_func(*args):
    fs = []
    for i in range(3):
        func = lambda : i * i
        fs.append(func)
    return fs

  

  经过上面的分析,我们得出下面一个重要的经验:返回闭包中不要引用任何循环变量,或者后续会发生变化的变量。

  这条规则本质上是在返回闭包前,闭包中引用的父函数中定义变量的值可能会发生不是我们期望的变化。

def my_func(*args):
    fs = []
    for i in range(3):
        def func(_i = i):
            return _i * _i
        fs.append(func)
    return fs

#或者
def my_func(*args):
fs = []
for i in range(3):
func = lambda _i = i : _i * _i
fs.append(func)
return fs

  

  正确的做法便是将父函数的local variable赋值给函数的形参。函数定义时,对形参的不同赋值会保留在当前函数定义中,不会对其他函数有影响。

  另外注意一点,如果返回的函数中没有引用父函数中定义的local variable,那么返回的函数不是闭包函数。

闭包函数的应用:https://www.cnblogs.com/yssjun/p/9887239.html

 

 

 

 

 

 

 

 

 

 

  

 

 

 

posted @ 2019-06-15 17:50  HelloBaby!  阅读(756)  评论(0编辑  收藏  举报