对于Golang和Python的新手来说,本文解释了什么是闭包,以及在使用这两种编程语言的闭包时需要注意的一些技巧。为了方便大家复制和在自己的服务器上运行,代码部分提供了包、导入和其他重复的部分。负面方面是,这导致了某些冗余,因此如果这给您带来不便,请理解。
什么是闭包?
闭包是存储函数及其环境的记录。
闭包的两个要素:函数和环境
- 函数:指的是在闭包实际实现时,通常通过调用外部函数返回其内部函数来完成。内部函数可以是内部具名函数、匿名函数或lambda表达式。
- 环境:在实践中,引用的环境是外部函数的环境,闭包在生成时保存/记录了外部函数的所有环境。
总结来说,闭包是一个扩展作用域的函数,它保留了定义时存在的自由变量(未在局部作用域中绑定的变量)的绑定,因此当调用该函数时,即使定义的作用域不再可用,这些绑定仍然可以使用。以下是闭包的常见用法:
// golang 示例
package main
import "fmt"
func outer() func(v int) {
x := 1
return func(v int) {
y := x + v
fmt.Println(x)
fmt.Println(y)
}
}
func main() {
a := outer()
a(1)
a(2)
}
// 结果
1
2
1
3
在Golang示例中的逻辑中,“a”是一个闭包,闭包函数是内部的func(v int){}, 闭包环境是外部的“x”,由于外部环境被“捕获”,因此每次执行闭包时“x”都是1,最终的输出结果是1,2,1,3。
# python 示例
def outer():
x = 1
def inner(v):
y = x + v
print(x)
print(y)
return inner
a = outer()
a(1)
a(2)
# 结果
1
2
1
3
在Python示例中,相同的闭包函数是inner(v),闭包环境是“x”,因此每次执行闭包时“x”都是1(因为闭包环境被捕获),然后闭包内的逻辑是将“x”与闭包传递的参数相加,所以最终输出是1,2,1,3。
Golang中的闭包
匿名函数
由于在Golang中匿名函数使用非常频繁,我们从这里开始:匿名函数及其“捕获”的自由变量称为闭包,无论是正常使用、在for循环中使用,还是在defer中使用,它们都是闭合的:
package main
import "fmt"
func main() {
x := 1
a := func(v int) {
y := x + v
fmt.Println(x)
fmt.Println(y)
}
a(2)
a(3)
}
// 结果
1
3
1
4
在上面的例子中,a是一个闭包,闭包函数是匿名函数func(v int){}, 闭包环境是x,因此结果是1,3,1,4。
package main
import "fmt"
func main() {
y := 2
defer func() {
fmt.Println(y)
}()
}
// 结果
2
defer中的匿名函数同样如此,defer中的func(){}是匿名函数,闭包环境是“y”,所以输出为2。
修改闭包环境
首先,Golang中的所有引用传递都是值传递,如果有类似引用传递的情况(文章后面会提到,为了方便),实际上是传递了底层指针的“值”,从而实现了所谓的引用传递。
- 如果要在闭包内部(闭包函数内)修改闭包环境,Golang很容易做到。由于Golang是一种声明式语言,赋值和声明的写法不同(:=用于声明,=用于赋值);并且Golang闭包“捕获”闭包环境的本质是引用传递而不是值传递,所以可以直接修改,如下例所示:
package main
import "fmt"
func make_avg() func(v int) {
count := 0
total := 0
return func(v int) {
count += 1
total += v
fmt.Println(float32(total)/float32(count))
}
}
func main() {
a := make_avg()
a(1)
a(2)
a(3)
}
// 结果
1
1.5
2
这个例子计算平均值,闭包是“a”,闭包函数是内部的func(v int){}匿名函数,闭包环境是“count”和“total”;count += 1,total += v是直接修改闭包环境的行为,并获得预期效果。
特别是,你可以利用Golang闭包“捕获”闭包环境的本质,即引用传递,在匿名函数内部(闭包函数内部)修改全局变量(闭包环境),如下例所示:
package main
import (
"fmt"
)
var x int = 1
func main() {
a := func() {
x += 1
}
fmt.Println(x)
a()
fmt.Println(x)
}
// 结果
1
2
- 在闭包外部修改闭包环境
你可能会有疑问,“在闭包外部”可以修改闭包环境吗?实际上,在Golang中是可能的,请记住以下两句话:
- 如果外部函数的所有变量都是局部的,即生命周期在外部函数结束时结束,那么闭包的环境也是封闭的。
- 相反,如果闭包环境可以通过指针修改,那么可以从闭包外部修改闭包环境,请看以下示例:
package main
import "fmt"
func foo1(x *int) func() {
return func() {
*x = *x + 1
fmt.Println(*x)
}
}
func foo2(x int) func() {
return func() {
x = x + 1
fmt.Println(x)
}
}
func main() {
x := 133
f1 := foo1(&x)
f2 := foo2(x)
f1()
f1()
f2()
f2()
x = 233
f1()
f1()
f2()
f2()
foo1(&x)()
foo1(&x)()
foo2(x)()
foo2(x)()
}
两个闭包内部的逻辑需要分析,一个是指针变量的和,根据我们之前所说的“闭包环境可以通过指针修改”,每次执行闭包或在外部直接赋值都会真正改变变量的值,而不使用指针的“foo2”则是普通的闭包,即闭包环境仅在闭包内部;因此,前四组输出如下:
134
135
134
135
中间四组是由“f1”闭包在被外部强制修改为233后进行累加,而“f2”闭包则在其自身环境中进行累加,因此输出为:
234
235
136
137
最后四组生成了四个新的闭包,因此“foo1”部分仍然基于当前“x”值进行累加,累加值实际上作用于全局变量“x”;foo2中的累加仍然是在其自身闭包内进行,因此输出为:
236
237
238
238
通过这个例子,我们区分了从闭包内部和从闭包外部修改闭包环境。
闭包的延迟绑定
这个问题是每个Golang新手都会遇到的一个非常令人困惑的问题;请记住以下一句话:在执行闭包时,闭包环境声明周期得到保证,并且会去外部环境查找最新的闭包环境(值),以下示例中,在执行闭包时“i”是闭包环境,执行闭包时最新值已经是10,因此所有输出都是10。
package main
import "fmt"
func main() {
var handlers []func()
for i := 0; i < 10; i++ {
handlers = append(handlers, func() {
fmt
.Println(i)
})
}
for _, handler := range handlers {
handler()
}
}
// 结果
10
10
10
10
10
10
10
10
10
10
解决方法是在for循环中复制一个不被闭包引用的环境变量,然后使用该值代替闭包环境,修改后的版本如下:
package main
import "fmt"
func main() {
var handlers []func()
for i := 0; i < 10; i++ {
// a不是闭包环境,因为每次都会重新声明
a := i
handlers = append(handlers, func() {
fmt.Println(a)
})
}
for _, handler := range handlers {
handler()
}
}
// 结果
0
1
2
3
4
5
6
7
8
9
实际上,原理很清楚,这不是关于for循环的问题,正常使用、defer使用都会受到此原则的约束,执行闭包时,闭包环境声明周期得到保证,并且会去外部环境查找最新的闭包环境(值)
package main
import "fmt"
func main() {
x, y := 1, 2
defer func(a int) {
fmt.Println(a, y)
}(x)
x += 100
y += 100
}
// 输出, y 是闭包环境,所以执行闭包时会查找最新值,而 a 不是闭包环境,复制了 x 的值,所以不受影响
1 102
在 go 协程中使用匿名函数是一个常见的场景,并且会遇到这个问题,见下面的示例:
package main
import (
"fmt"
"time"
)
func show(val int) {
fmt.Println(val)
}
func main() {
values := []int{1, 2, 3, 5}
for _, val := range values {
go show(val)
}
time.Sleep(time.Second)
}
// 每次都会输出四个 1,2,3,5 虽然顺序不同,因为没有使用匿名函数,它不是闭包
5
1
3
2
每次都会输出四个1,2,3,5,虽然顺序不同,因为没有使用匿名函数,它不是闭包。
package main
import (
"fmt"
"time"
)
func main() {
values := []int{1, 2, 3, 5}
for _, val := range values {
go func(){
fmt.Println(val)
}()
}
time.Sleep(time.Second)
}
// 输出
5
5
5
5
修改方法与for循环示例相同,使用传递变量来避免闭包环境。
package main
import (
"fmt"
"time"
)
func main() {
values := []int{1, 2, 3, 5}
for _, val := range values {
go func(val int){
fmt.Println(val)
}(val)
}
time.Sleep(time.Second)
}
// 输出
1
5
3
2
Python中的闭包
修改闭包环境
- 从内部修改闭包环境
由于Python不是声明式语言,一个“=”涵盖所有,因此我们需要使用nonlocal参数显式声明变量为闭包环境,而不是局部变量,如下例所示,如果像Golang那样直接更改,有时会出现问题。
使用Python列表作为闭包环境
def make_avg(count, total):
count = []
def avg(v):
count.append(v)
total = sum(count)
print(sum(count)/len(count))
return avg
a = make_avg(0, 0)
a(1)
a(2)
a(3)
# 输出
1.0
1.5
2.0
使用Python字符作为闭包环境
def make_avg(count, total):
count = 0
total = 0
def avg(v):
count += 1
total += v
print(total/count)
return avg
a = make_avg(0, 0)
a(1)
a(2)
a(3)
# 错误
Traceback (most recent call last):
File "/root/code/linux/blog/aa.py", line 14, in <module>
a(1)
File "/root/code/linux/blog/aa.py", line 7, in avg
count += 1
UnboundLocalError: local variable 'count' referenced before assignment
这是因为Python有两种数据类型,一种是可变数据类型,一种是不可变数据类型,传递变量数据类型时使用引用传递,这与Golang闭包直接修改特性相匹配,但不可变数据类型(如上面的字符串)会出现问题,因为Python会将其视为新生成的局部变量,因此解决方法是使用nonlocal告诉Python解释器,这个变量是闭包环境,这样就可以正常运行,正确的写法如下:
def make_avg(count, total):
count = 0
total = 0
def avg(v):
nonlocal count, total
count += 1
total += v
print(total/count)
return avg
a = make_avg(0, 0)
a(1)
a(2)
a(3)
# 输出
1.0
1.5
2.0
闭包和装饰器
我本来想谈谈Python装饰器,但由于篇幅原因,决定留待以后再说。
总结
上面有很多例子,读完每个例子后,理解为什么,然后闭包问题就会解决了;
请记住,由于闭包不常用,且实用性不高,如果工作中不必要使用闭包,你可以不使用闭包。否则可能会引发一些内存泄漏,这不是一个小问题。
- 闭包的两个主要元素:函数和环境
- 闭包是一个扩展作用域的函数,它会保留定义时存在的自由变量(未在局部作用域中绑定的变量)的绑定,因此当调用该函数时,定义的作用域不可用,但仍可以使用这些绑定
- 在Golang中使用匿名函数实际上就是一个闭包
- 通过指针可以从闭包外部修改闭包环境变量
- Golang 闭包延迟绑定问题:执行闭包时,闭包环境声明周期得到保证,并且会去外部环境查找最新的闭包环境(值)
- Python闭包使用nonlocal声明变量为闭包环境,而不是局部变量
上述分析是我对闭包的理解,希望Golang、Python新手在阅读后可以避免踩到必须踩的坑,如果对环境有疑问,请随时留言或发送电子邮件联系,谢谢。
参考链接