浅谈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

 

posted on 2021-03-05 17:31  江湖乄夜雨  阅读(533)  评论(0编辑  收藏  举报