Python闭包详解:理解闭包与可变类型和不可变类型的关系
一、定义
闭包(Closure) 指的是一个函数对象,即使其外部作用域的变量已经不存在了,仍然能访问这些变量。简单来说,闭包是由函数及其相关的环境变量组成的实体。
def outer():x = 10def inner():print(x)return innerf = outer()
f() # 输出10
对于这个示例,我们可以将inner函数称为闭包,它满足了嵌套函数,它满足了使用外部函数outer的参数x,外部函数的返回值满足了是inner函数本身
二、闭包
(1)闭包的形成条件
闭包必须是一个嵌套函数,并且内部函数引用了外的变量,外部函数的返回值是该内部函数,对于这个引用外部函数变量的内部函数称为闭包。
- 必须有嵌套函数(函数内部定义函数)
- 内部函数引用了外部函数的变量
- 外部函数返回内部函数对象
示例1:没有嵌套函数,只有单层函数
def func():x = 10print(x)func()
示例2:嵌套函数存在,但内部函数没有引用外部变量
def outer():x = 10def inner():print("Hello")return innerf = outer()
f()
示例3:嵌套函数引用了外部变量,但外部函数没有返回该内部函数
def outer():x = 10def inner():print(x)inner() # 调用内部函数,但没有返回它outer()
(2)闭包的使用场景
1)数据隐藏和封装:闭包可以把一些变量藏在函数里面,不让外面直接访问,这样就不会乱用或误改,避免弄乱全局变量,让代码更安全。
2)装饰器实现:装饰器本质上依赖闭包机制
3)函数工厂:闭包可以帮你根据不同需求,快速生成带有特定设置或环境的函数,就像工厂按订单生产不同产品一样。
def make_multiplier(factor):# 这是一个函数工厂,传入一个倍数 factordef multiplier(number):# multiplier 是闭包,记住了外面的 factorreturn number * factorreturn multiplier# 生成一个把数字乘以3的函数
times3 = make_multiplier(3)
print(times3(5)) # 输出 15# 生成一个把数字乘以10的函数
times10 = make_multiplier(10)
print(times10(5)) # 输出 50
(3)闭包的作用与优势
1)减少全局变量使用,提升代码安全性
闭包让变量“藏”在函数里,避免把变量放到全局,减少冲突和错误,让代码更可靠。
2)保持函数运行环境,方便管理状态
闭包可以记住外部变量的值,即使外部函数已经结束,内部函数还能继续用这些数据,方便管理和维护程序状态。
三、Python中的可变类型与不可变类型
3.1 变量类型
(1)不可变类型
包括:int、float、str、tuple、frozenset等,对象一旦创建,值不能被改变,任何修改都会生成新对象。
(2)可变类型
包括:list、dict、set、自定义类对象等,对象创建后,内容可以被修改,地址不变。
3.2 两者区别与内存表现
1)不可变类型变量修改时,实际是创建了新的对象,变量指向新地址
a = 10
print(id(a)) # 假设输出:140703079708016a = a + 1
print(id(a)) # 输出:140703079708048 (地址发生变化)
说明:变量 a
原来指向值为10的对象,修改后指向了新创建的值为11的对象,地址发生变化。
2)可变类型变量修改时,变量指向的对象地址不变,内容发生变化
lst = [1, 2, 3]
print(id(lst)) # 假设输出:140703080123456lst.append(4)
print(id(lst)) # 输出:140703080123456 (地址未变)
print(lst) # 输出:[1, 2, 3, 4]
说明:变量 lst
指向的列表对象地址没有变,但列表内部内容发生了变化。
3)通过id()
函数可以观察变量地址变化。id()
返回对象的内存地址标识,可以用来判断变量是否指向同一个对象。
3.3 赋值和修改对变量的影响
1)对不可变类型变量赋值,变量绑定新对象,不影响原对象
2)对可变类型变量修改,直接改变对象内容,所有引用该对象的变量都能感知变化
四、闭包中变量的可变性影响
4.1 闭包对不可变类型变量的访问与限制
闭包内部访问外部不可变变量时,如果尝试修改,会报错(UnboundLocalError),因为修改会被当作局部变量赋值,导致访问冲突。
def outer():x = 10 # 外部不可变变量def inner():x += 5 # 尝试修改外部变量,报错!print(x)inner()outer()
解释:
在 inner 函数里,写了 x += 5,这相当于想给 x 重新赋值。Python 看到这个,就把 x 当成是 inner 里的“新变量”,而不是外面那个已经有值的 x。但是这个“新变量”还没被定义,结果你又想用它来计算,就出错了。
简单说,就是闭包里面如果你想修改外面那个数字,Python 会误以为你是在用一个自己新建但还没给值的变量,所以会报错。要想修改外面的变量,需要告诉 Python “嘿,我用的是外面的那个变量”,这时候就得用 nonlocal。
4.2 使用 nonlocal 关键字修改不可变变量
nonlocal 的作用就是告诉 Python:“我想用的是外面函数里的那个变量,不是新建一个新的。”
所以在 inner 里写了 nonlocal x,Python 就知道你要改的是外面 outer 函数里的 x,然后你给它加 5,修改成功了。这样,inner 里和外面打印的 x 都变成了 15,因为它们指的是同一个变量。
def outer():x = 10def inner():nonlocal xx += 5print(x)inner()print(x)outer()
"""
输出:
15
15
"""
4.3 闭包中对可变类型变量的修改与访问
对可变类型变量,闭包内部可以直接修改其内容,无需nonlocal。当然建议使用nonlocal关键字声明。
def outer():lst = [1, 2, 3]def inner():lst.append(4)print(lst)inner()print(lst)outer()
"""
输出
[1, 2, 3, 4]
[1, 2, 3, 4]"""
为什么修改lst不需要使用nonlocal关键字呢。由于lst是一个列表他是一个可变类型,内层函数在对lst进行添加操作时,并没有改变lst本身,它所指向的内存空间也没有发生改变。如果内层函数代码为del lst 想要删除这个列表,也就是对外层函数的lst做了修改操作,此时就会发生报错UnboundLocalError,如果加上关键字nonlocal进行声明,就可以对它进行删除。
4.4 常见误区
1)不是所有闭包里的变量修改都要用 nonlocal
def outer():lst = [1, 2, 3] # 可变类型变量def inner():lst.append(4) # 直接修改列表内容,无需nonlocalprint(lst)inner()print(lst)outer()
2)别把“换变量”和“改内容”搞混了
def outer():x = 10 # 不可变类型lst = [1, 2, 3] # 可变类型def inner():# x += 1 # 重新绑定,会报错,需要nonlocal# 改成下面这样才合法:nonlocal xx += 1# 对列表内容修改,不是重新绑定lst.append(4)print(x, lst)inner()print(x, lst)outer()
3)别忽略了可变对象改了内容,闭包里的变量也跟着变
def outer():d = {'count': 0} # 可变字典def inner():d['count'] += 1 # 修改字典内容print(d['count'])return innerf = outer()
f() # 输出1
f() # 输出2