浅谈Python与Golang中的“惰性求值”
前言
Python与Golang中的“惰性求值”主要出现在闭包中。
本文主要通过几个简单案例介绍一下Python中闭包的惰性求值与Golang中闭包与Goroutine的惰性求值机制与理解。
Python中闭包的惰性求值
简单的案例
先来看一个使用Python实现闭包惰性求值的简单案例:
def outer(): x = 1 def inner(): print(x) x = 123 return inner f = outer() f() # 123
我们可以看到,闭包inner在outer执行时记录了产生它的时候外部环境的所有环境(其实就是变量x),然后在执行闭包(执行f())的时候寻找外部环境最新的那个值(很显然,x的最新的值是123),所以程序最终会打印123!
这就是闭包十分神奇的地方:闭包会保存外部引用环境!(如果按照常规思路来理解,在执行inner时外部x变量的生命周期按理说已经结束,inner函数中没有x会报错...)
返回lambda匿名函数列表
def gen_func_list() -> list: # 匿名函数的输入是x与y return [lambda x, y: (x + y) * i for i in range(3)] # 注意返回的是匿名函数组成的列表 func_lst = gen_func_list() print("func_lst: ", func_lst) """ [<function gen_list.<locals>.<listcomp>.<lambda> at 0x7fe1622e9ea0>, <function gen_list.<locals>.<listcomp>.<lambda> at 0x7fe1622e9840>, <function gen_list.<locals>.<listcomp>.<lambda> at 0x7fe1622e9f28>] """ for func in func_lst: # 匿名函数需要2个参数 print(func(1, 2))
## 结果:
# 6
# 6
# 6
通过上面那个例子,这个案例的输出也十分容易理解了:闭包在执行时会寻找外部环境最新的值,很显然 for range循环最新的值时2,所以所有函数都会打印 (1+2)*2,结果是6。
Golang中闭包与Goroutine的惰性求值
闭包的惰性求值
与Python一样,我们先来看一个简单的案例:
package test1 import ( "fmt" "testing" ) // 闭包的惰性求值 func fooClosure() func(){ x := 1 f := func(){ fmt.Printf("fooClosure val = %d\n", x) } x = 123 return f } func TestClosure(t *testing.T){ f8 := fooClosure() f8() // fooClosure val = 123 }
与Python相同,Golang中的闭包也会保存外部环境,在闭包执行阶段会寻找外部环境最新的值处理。
与上面的返回lambda函数列表对应的一个例子:
package test1 import ( "fmt" "testing" ) // case7:闭包的惰性求值 func foo7(x int) []func() { var fs []func() values := []int{1, 2, 3, 5} for _, val := range values { fs = append(fs, func() { fmt.Printf("foo7 val = %d\n", x+val) }) } return fs } func TestFoo7(t *testing.T) { f7s := foo7(11) for _, f7 := range f7s { f7() } } /* foo7 val = 16 foo7 val = 16 foo7 val = 16 foo7 val = 16 */
另外一个模仿lambda表达式的例子:
func genFuncList() []func(x, y int) int { var retLst []func(x,y int)int valueLst := []int{2,3,4} // value这个系数~最终都是4:参与了运算!!! // 但是:最终参与运算的,是最后的值~~ 4!!!惰性求值机制的使用!!! for _, value := range valueLst{ currentFunc := func(x,y int)int{ return (x+y)*value } retLst = append(retLst, currentFunc) } return retLst } func TestDemo2(t *testing.T){ x, y := 1,2 funcLst := genFuncList() for _, currFunc := range funcLst{ fmt.Println(currFunc(x,y)) } //=== RUN TestDemo2 //12 = (1+2) * 4 //12 //12 }
~~~
Goroutine的惰性求值
在Golang中,Goroutine也有惰性求值的机制,我们来看看下面这个例子:
package test1 import ( "fmt" "sync" "testing" ) // goroutine的惰性求值 func foo5(){ wait := sync.WaitGroup{} values := []int{1,2,3,5} for _, val := range values{ wait.Add(1) go func(){ defer wait.Done() fmt.Println("foo5 value = ", val) }() } // wait wait.Wait() } func TestFoo5(t *testing.T){ foo5() /* foo5 value = 5 foo5 value = 5 foo5 value = 5 foo5 value = 5 */ }
其实这个问题的本质同闭包的惰性求值,或者说,这段匿名函数的对象就是闭包。在我们调用go func() { xxx }()
的时候,只要没有真正开始执行这段代码,那它还只是一段函数声明。而在这段匿名函数被执行的时候,才是内部变量寻找真正赋值的时候!
在上面的case中,for-range的遍历几乎是“瞬时完成的”,4个Go Routine真正被执行在其后。但是按照我们正常的逻辑来看:val的生命周期按理说已经结束了呀!程序不应该报错吗?
其实根据上面介绍的“闭包”的思路:goroutine并发执行的函数其实也是一个闭包!那么闭包在真正被执行的时候,即使for-range结束,但是在闭包中会保存外部环境val的值,并且每次都会使用最新的val,也就是5!
参考文章
https://zhuanlan.zhihu.com/p/92634505