Yiner

Apr 03, 2022

Python 闭包&全局变量

 
 
 

参考链接

理解Python闭包概念
闭包并不只是一个python中的概念,在函数式编程语言中应用较为广泛。理解python中的闭包一方面是能够正确的使用闭包,另一方面可以好好体会和思考闭包的设计思想。 首先看一下维基上对闭包的解释: 在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。 简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包。实际上闭包可以看做一种更加广义的函数概念。因为其已经不再是传统意义上定义的函数。 根据我们对编程语言中函数的理解,大概印象中的函数是这样的: 程序被加载到内存执行时,函数定义的代码被存放在代码段中。函数被调用时,会在栈上创建其执行环境,也就是初始化其中定义的变量和外部传入的形参以便函数进行下一步的执行操作。当函数执行完成并返回函数结果后,函数栈帧便会被销毁掉。函数中的临时变量以及存储的中间计算结果都不会保留。下次调用时唯一发生变化的就是函数传入的形参可能会不一样。函数栈帧会重新初始化函数的执行环境。 C++中有static关键字,函数中的static关键字定义的变量独立于函数之外,而且会保留函数中值的变化。函数中使用的全局变量也有类似的性质。 但是闭包中引用的函数定义之外的变量是否可以这么理解呢?但是如果函数中引用的变量既不是全局的,也不是静态的(python中没有这个概念)。应该怎么正确的理解呢? 建议先参考一下我的另一篇博文( Python UnboundLocalError和NameError错误根源解析 ),了解一下变量可见性和绑定相关的概念非常有必要。 为了说明闭包中引用的变量的性质,可以看一下下面的这个例子: 程序的运行结果: clo_func_0 loc_list = [1]clo_func_0 loc_list = [1, 2]clo_func_0 loc_list = [1, 2, 3]clo_func_1 loc_list = [1]clo_func_0 loc_list = [1, 2, 3, 4]clo_func_1 loc_list = [1, 2] 从上面这个简单的例子应该对闭包有一个直观的理解了。运行的结果也说明了闭包函数中引用的父函数中local variable既不具有C++中的全局变量的性质也没有static变量的行为。 在python中我们称上面的这个loc_list为闭包函数inner_func的一个自由变量(free variable)。 If a name is bound in a block, it is a local variable of that block.
 

变量作用域

Python变量的作用域一共有4种,分别是:
  • L (Local) 局部作用域
  • E (Enclosing) 闭包函数外的函数中
  • G (Global) 全局作用域
  • B (Built-in) 内建作用域
💡
Python会以 L –> E –> G –>B 的顺序查找,首先在局部作用域内查找变量,如果在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局作用域找,最后再去内建作用域中找。
 
  • 优先引用的是局部变量,即范围最小最近的。
x=1 # 全局变量 G def outer_func(): x=2 # 闭包的环境变量 E print(x) def inner_func(): x = 3 # 局部变量 L print(x) return inner_func print(x) # 打印全局变量 f = outer_func() # 执行outer_func函数,打印outer函数的局部变量x=2,返回inner_func函数 f() # 执行inner_func函数,打印outer函数的局部变量x=3 #=========输出结果============= >>> print(x) 1 >>> f = outer_func() 2 >>> f() 3
  • 在构成闭包的情况,如果内部函数没有局部变量,则会优先引用闭包的环境变量。
x=1 # 全局变量 G def outer_func(): x=2 # 闭包的环境变量 E def inner_func(): print(x) return inner_func f = outer_func() f() #=========输出结果============= >>> f() 2
 

闭包概念

维基百科:
闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。
闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
环境里是若干对符号和值的对应关系,它既要包括约束变量 (该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。
闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行
捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定。
 
💡
闭包的概念:闭包 = 函数 + 环境变量(自由变量)
  • 在上面的代码例子中, inner_funcx = 2 形成了闭包。通俗的说就是把内部函数  inner_func与 x = 2 这个环境变量包含在了一起,做了一个封闭,同时外界想要去改变 x 这个变量是改变不了的。
    • 红色框就是闭包
      红色框就是闭包
  • 需要注意的是,所谓的环境变量,一定要定义在内部函数的外部(外部函数内的局部变量),就像 x = 2一样,且不能是全局变量!
 
 

查看闭包

📌
通过调用 __closure__ 内置方法可以查看到两个内存地址,结果返回cell就是闭包,None 则不是闭包。
 
  • 是闭包的情况
    • 📌
      可以看出来其实这是一个元组类型,使用__closure__[0].cell_contents可以得到闭合数值,也就闭包所需要的环境变量
      def outer_func(): x=2 # 闭包的环境变量 E def inner_func(): print(x) return inner_func f = outer_func() #=========输出结果============= >>> f.__closure__ (<cell at 0x7fd6d0090b80: int object at 0x7fd6f002e970>,) # 元组 >>> f.__closure__[0] <cell at 0x7fd6d0090b80: int object at 0x7fd6f002e970> # cell >>> f.__closure__[0].cell_contents 3 # 环境变量
  • 不是闭包的情况,调用 __closure__ 返回None
    • def outer_func(): def inner_func(): x=3 # 局部变量 L print(x) return inner_func f = outer_func() #=========输出结果============= >>> print(f.__closure__) None >>> print(f.__closure__[0]) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'NoneType' object is not subscriptable

易错细节

📌
直接在内部函数修改其环境变量全局变量会报错!!
x = 1 def func(): x = x + 1 print(x) func() #=========输出结果============= >>> func() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in func UnboundLocalError: local variable 'x' referenced before assignment
def outer_func(): x=2 # 闭包的环境变量 E def inner_func(): x = x + 1 print(x) return inner_func f = outer_func() f() #=========输出结果============= >>> f = outer_func() >>> f() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in inner_func UnboundLocalError: local variable 'x' referenced before assignment
💡
原因是,内部函数有引用外部函数的同名变量(即闭包中的环境变量)或者全局变量,并且对这个变量有修改的时候,此时 Python 会认为它是一个局部变量,而函数中并没有 x 的定义和赋值,所以报错。

修改全局变量

📌
解决方法:为需要修改的全局变量global关键词添加显式声明,这样明显的告诉编译器,我就是要修改全局变量。
使用这种显式声明的方式也意味着程序员要自己为这个变量负责,因为擅自修改更大作用域范围的变量是很容易产生难以察觉的错误。
x = 1 def func(): global x # global声明全局变量 x x = x + 1 print(x) func() #=========输出结果============= >>> func() 2
 

修改闭包环境变量

📌
解决方法:为需要修改的环境变量nonlocal关键词添加显式声明,这样明显的告诉编译器,我就是要修改环境变量。
def outer_func(): x=2 # 闭包的环境变量 E def inner_func(): nonlocal x # nonlocal声明闭包的环境变量 x x = x + 1 print(x) return inner_func f = outer_func() f() #=========输出结果============= >>> f = outer_func() >>> f() 3
 

闭包多个实例互相独立

  • 闭包中的引用的自由变量只和具体的闭包有关联,闭包的每个实例引用的自由变量互不干扰
  • 一个闭包实例对其自由变量的修改会被传递到下一次该闭包实例的调用
def outer_func(): res=[] # 闭包的环境变量 E def inner_func(name): res.append(len(res) + 1) print(f"{name}'s res: {res}") return inner_func f1 = outer_func() # 实例1 f2 = outer_func() # 实例2 #=========输出结果============= >>> f1('f1') f1's res: [1] >>> f1('f1') f1's res: [1, 2] >>> f1('f1') f1's res: [1, 2, 3] >>> f2('f2') f2's res: [1] >>> f2('f2') f2's res: [1, 2]
  • f1f2的自由变量res之间互相独立。
 
 

自由变量确定的时机

从上面维基百科的定义能看出,“当捕捉闭包的时候,它的自由变量会在捕捉时被确定”。
💡
闭包是在被返回的时候确定自由变量的值。故在返回闭包前,闭包中引用的父函数中定义变量的值可能会发生不是我们期望的变化。
 

错误做法

def outer_func(*args): fs = [] for i in range(3): def inner_func(): return i * i fs.append(inner_func) return fs # 此时i=2 fs1, fs2, fs3 = outer_func() print(fs1()) print(fs2()) print(fs3()) #=========输出结果============= >>> print(fs1()) 4 >>> print(fs2()) 4 >>> print(fs3()) 4
  • 出现上述结果原因:在返回闭包列表fs之前for循环的变量i的值已经发生改变了,而且这个改变会影响到所有引用它的内部定义的函数,所以在返回闭包列表fs时,i=2,故最后调用3个闭包函数时,结果都为4。
  • 此处inner_func改成匿名函数结果也是相同的。
    • def inner_func(): return i * i fs.append(inner_func) # 变为 func = lambda : i * i fs.append(inner_func)
📌
注意:返回闭包中不要引用任何循环变量,或者后续会发生变化的变量。
 

正确做法

def outer_func(*args): fs = [] for i in range(3): def inner_func(_i=i): return _i * _i fs.append(inner_func) return fs # 或lambda函数写法 def outer_func(*args): fs = [] for i in range(3): func = lambda _i = i : _i * _i fs.append(func) return fs fs1, fs2, fs3 = outer_func() print(fs1()) print(fs2()) print(fs3()) #=========输出结果============= >>> print(fs1()) 0 >>> print(fs2()) 1 >>> print(fs3()) 4
 
📌
正确做法:将父函数的局部变量i赋值给函数的形参_i。函数定义时,对形参的不同赋值会保留在当前函数定义中,不会随后期i的变化而变化。
  • 另外注意一点,如果返回的函数中没有引用父函数中定义的局部变量,那么返回的函数不是闭包函数。
 

闭包应用

  • 自由变元可以记录闭包函数被调用的信息,以及闭包函数的一些计算结果中间值。而且被自由变量记录的值,在下次调用闭包函数时依旧有效。
  • 根据闭包函数中引用的自由变量的一些特性,闭包的应用场景还是比较广泛的,例如装饰器、单例模式等。

装饰器

如果我们想对一个函数或者类进行修改重定义,最简单的方法就是直接修改其定义。但是这种做法的缺点也是显而易见的:
  • 可能看不到函数或者类的定义
  • 会破坏原来的定义,导致原来对类的引用不兼容
  • 如果多人想在原来的基础上定制自己函数,很容易冲突
💡
可以使用装饰器对需要修改的函数或类进行封装,达成不破坏原先定义的作用。
def func_dec(func): def wrapper(args): if len(args) == 2: func(args) else: print(f'Error! Arguments = {args}') return wrapper @func_dec def add_sum(args): print(sum(args)) # add_sum = func_dec(add_sum) args1 = [1, 2] args2 = [1] add_sum(args1) add_sum(args2) #=========输出结果============= >>> add_sum(args1) 3 >>> add_sum(args2) Error! Arguments = [1]
  • 对于上面的这个例子,并没有破坏add_sum函数的定义,只不过是对其进行了一层简单的封装。
  • 如果看不到函数的定义,也可以对函数对象进行封装,达到相同的效果(即上面注释掉的13行),而且装饰器是可以叠加使用的。
 
关于装饰器的潜在问题具体见以下文章4.1部分:https://www.cnblogs.com/yssjun/p/9887239.html
 
 
 

Copyright © 2024 Yiner

logo