参考链接
变量作用域
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_func
与x = 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]
f1
和f2
的自由变量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