Python闭包再研究

前两天写了一篇文章,讲了一下Python的闭包。刚好今天又看到一个小问题,和Python闭包有点相关。顺手记录下来。

 

如下一段代码,

 

 

funcs = []
for i in xrange(10):
    def bar(n):
        return n + i
    funcs.append(bar)

print funcs[3](5)

 

这段代码中,我们期望得到的结果是3+5为8。但是实际得到的结果是什么呢?是14。

 

14是怎么来的?

 

反汇编看看:

 

 

  7           0 LOAD_FAST                0 (n)
              3 LOAD_GLOBAL              0 (i)
              6 BINARY_ADD
              7 RETURN_VALUE  

 

注意i是global。

 

得到14的原因就是,funcs[3]这个函数对象获取i的值,是在执行的时候。而i的作用域是global。也就是说,当这个func开始执行的时候,i已经变成9了。那么结果当然等于14了。

 

从这个结果看,以上代码和下面代码效果是等价的。

 

 

funcs = []
for i in xrange(10):
    pass

def bar(n):
    return n + i

funcs.append(bar)#这句重复10遍
print funcs[3](5)

 

很无趣吧。那么考虑一下,如果把i丢到闭包来做会怎样?

 

 

funcs = []
def foo(m):
    for i in xrange(m):
        def bar(n):
            return n + i
        funcs.append(bar)
foo(10)
print funcs[3](5)

 

很遗憾,结果依然是14. 

 

反汇编代码如下:

 

 

  9           0 LOAD_FAST                0 (n)
              3 LOAD_DEREF               0 (i)
              6 BINARY_ADD
              7 RETURN_VALUE 

 

唉,只是傻傻的远程访问而已。

 

“所有的bar代码中,i仅仅只是在closure中的一个引用而已。指向的依然是同一个对象。当这个对象被改变,所有的bar执行的时候获得的值都是修改后的值”。

 

顺手写了一段JavaScript来测试,发现结果是一样的。也是会全局改变。具体代码如下:

 

 

但是用haskell实现了一个,完全符合预期的结果。

 

 

main = do
    let funcs = [(\n -> n + i) | i <- [1..10] ]
    let x : xs = funcs
    return (x 4)
 

返回结果是5。

 

看来Python对FP的支持还是比不上Haskell这种正统的函数式语言。

 

个人觉得如果Python要发展FP的话,可以考虑如下解决方案:区别本地变量和闭包变量。当声明函数的时候将闭包变量拷贝一份到本地,同时保留指向原闭包对象的引用。在搜索名字的时候,始终都是先搜索本地变量,再搜索本地闭包变量副本,再搜索全局变量。当需要修改的时候,如果是本地变量就直接修改。如果是闭包变量的时候,将原来闭包真正指向的对象进行修改,同时覆盖掉本地的闭包副本。

 

这个方法只是暂时的考虑,没有仔细的推敲。乍看之下似乎可以解决问题。

 

不过,Python毕竟是OO的语言。没有Haskell或者Lisp那种天生的作用域的控制能力。唉。那就用OO的方式来搞Python把。

 

<think> 嗯,用户这次想深入了解Python中的闭包,结合之前的装饰器讨论,看来ta在系统学习Python高级特性。用户提供的引用资料已经涵盖了闭包的核心概念和应用场景,但需要更结构化的整合。 从引用[3]那个加法器的例子看,用户可能更倾向直观理解而非学术定义。ta之前研究过装饰器,而闭包正是装饰器的实现基础,这个连接点需要强调。另外引用[5]提到global关键字,说明用户可能混淆过变量作用域,这点在讲解时需要澄清。 准备从四个维度展开:首先是闭包的本质(函数+环境),重点讲清词法作用域链;然后通过计数器案例展示状态保持能力;接着解析闭包在装饰器中的底层作用;最后提醒常见陷阱。特别是引用[4]的计数器例子非常典型,考虑把它升级成带初始值的版本。 注意到用户提供的闭包定义(内部函数引用外部作用域变量)来自引用[3],这个定义足够清晰,就以此为切入点。但需要补充两个关键点:1) 被引用的变量必须来自外层非全局作用域 2) 闭包的生命周期特性。ta之前接触过装饰器,用装饰器作为应用示例会很自然。 变量捕获部分是个难点,引用[5]显示用户遇到过作用域问题。考虑用对比案例说明:不用global时为何无法修改外部变量,而闭包通过nonlocal实现。这个知识点容易混淆,需要强调闭包捕获的是变量引用而非值拷贝。 最后保留引用标记时要特别注意,每个概念点需对应到具体引用源。比如装饰器与闭包的关系要标注[1][2],而计数器案例标注[4]。 </think>以下是Python闭包的详细解释,结合核心概念、实现原理和应用场景进行说明: --- ### 一、闭包的核心定义 闭包(Closure)指**内部函数捕获并保留了外部函数作用域中的变量**,即使外部函数已执行完毕。其本质是函数与其引用环境的组合体。需满足两个条件: 1. 存在函数嵌套(内部函数定义在外部函数内) 2. 内部函数引用了外部函数的变量 ```python def outer(x): # 外部函数 def inner(y): # 内部函数(闭包) return x + y # 引用外部变量x return inner # 返回闭包函数 add5 = outer(5) # x=5被闭包捕获 print(add5(3)) # 输出8 (5+3) ``` 此时`add5`是一个闭包,它记住了`x=5`的值[^3]。 --- ### 二、闭包的实现原理 #### 1. 词法作用域(Lexical Scoping) Python通过**LEGB规则**查找变量: - **L**ocal → **E**nclosing → **G**lobal → **B**uilt-in 闭包通过`__closure__`属性存储外部变量: ```python print(add5.__closure__[0].cell_contents) # 输出5(捕获的x值)[^2] ``` #### 2. 变量捕获机制 - **值捕获**:闭包捕获的是**变量的引用**,而非值的拷贝 - **生命周期延长**:外部函数执行后,其局部变量仍被闭包持有 ```python def counter(): count = 0 def increment(): nonlocal count # 声明使用外部变量 count += 1 return count return increment c = counter() print(c(), c(), c()) # 输出1, 2, 3(count状态被保留)[^4] ``` --- ### 三、闭包的应用场景 #### 1. 装饰器(Decorator) 装饰器本质是闭包的高级应用: ```python def logger(func): # 外部函数 def wrapper(*args): # 闭包函数 print(f"Calling {func.__name__}") return func(*args) return wrapper @logger def say_hello(name): print(f"Hello {name}!") say_hello("Alice") # 输出: # Calling say_hello # Hello Alice! ``` #### 2. 函数工厂(Function Factory) 生成定制化函数: ```python def power_factory(exponent): def power(base): return base ** exponent return power square = power_factory(2) cube = power_factory(3) print(square(4)) # 16 print(cube(3)) # 27 ``` #### 3. 状态保持 替代全局变量,实现安全的状态管理: ```python def private_counter(): __count = 0 # "私有"变量 def increment(): nonlocal __count __count += 1 return __count def decrement(): nonlocal __count __count -= 1 return __count return increment, decrement inc, dec = private_counter() print(inc()) # 1 print(dec()) # 0 ``` --- ### 四、闭包的陷阱与解决方案 #### 1. 延迟绑定问题 循环中创建闭包时的常见错误: ```python funcs = [] for i in range(3): def show(): print(i) funcs.append(show) for f in funcs: f() # 全部输出2(非预期的0,1,2) ``` **解决方案**:使用默认参数立即绑定 ```python for i in range(3): def show(j=i): # 立即捕获当前i值 print(j) funcs.append(show) ``` #### 2. 内存消耗 闭包会延长外部变量的生命周期,可能引发内存泄漏。对不再使用的闭包可显式解除引用: ```python del add5 # 手动释放闭包 ``` --- ### 五、闭包与作用域关键点 | 操作 | 关键字 | 影响范围 | |------------------|----------|-------------------| | 修改全局变量 | `global` | 全局作用域 | | 修改闭包变量 | `nonlocal` | 外层非全局作用域 | | 只读访问 | 无需声明 | 自动按LEGB查找 | ```python x = 10 def outer(): y = 20 def inner(): global x nonlocal y x += 1 # 修改全局x y += 1 # 修改闭包y return inner ``` --- ### 总结 闭包通过**函数嵌套 + 变量捕获**实现,核心价值在于: 1. 创建有状态的函数对象(如计数器) 2. 实现装饰器等高级模式(复用逻辑) 3. 封装私有数据(替代全局变量) 理解闭包需掌握LEGB作用域链和`__closure__`机制,同时注意避免延迟绑定陷阱[^1][^2][^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值