python高阶函数 闭包

高阶函数 Higher-order function

数学计算机科学中,高阶函数是至少满足下列一个条件的函数

  • 接受一个或多个函数作为输入
  • 输出一个函数

函数作为输入:

一个最简单的高阶函数:

def add(x, y, f):
    return f(x) + f(y)

当我们调用add(-5, 6, abs)时,参数xyf分别接收-56abs 

函数作为输出

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:

>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>

调用函数f时,才真正计算求和的结果:

>>> f()
25

在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。

 

请再注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False

f1()f2()的调用结果互不影响。

闭包 Closure

注意到返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。

另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。我们来看一个例子:

def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()

在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。

你可能认为调用f1()f2()f3()结果应该是149,但实际结果是:

>>> f1()
9
>>> f2()
9
>>> f3()
9

全部都是9!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9

 返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

 

def count():
    def f(j):
        def g():
            return j*j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
    return fs

再看看结果:

>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9

 深入理解闭包

什么是闭包?闭包有什么用?为什么要用闭包?今天我们就带着这3个问题来一步一步认识闭包。闭包和函数紧密联系在一起,介绍闭包前有必要先介绍一些背景知识,诸如嵌套函数、变量的作用域等概念

作用域

作用域是程序运行时变量可被访问的范围,定义在函数内的变量是局部变量,局部变量的作用范围只能是函数内部范围内,它不能在函数外引用。

def foo():
    num = 10 # 局部变量
print(num)  # NameError: name 'num' is not defined

定义在模块最外层的变量是全局变量,它是全局范围内可见的,当然在函数里面也可以读取到全局变量的。例如:

num = 10 # 全局变量
def foo():
    print(num)  # 10

嵌套函数

函数不仅可以定义在模块的最外层,还可以定义在另外一个函数的内部,像这种定义在函数里面的函数称之为嵌套函数(nested function)例如:

def print_msg():
    # print_msg 是外围函数
    msg = "zen of python"

    def printer():
        # printer是嵌套函数
        print(msg)
    printer()
# 输出 zen of python
print_msg()

对于嵌套函数,它可以访问到其外层作用域中声明的非局部(non-local)变量,比如代码示例中的变量 msg 可以被嵌套函数 printer 正常访问。

那么有没有一种可能即使脱离了函数本身的作用范围,局部变量还可以被访问得到呢?答案是闭包

什么是闭包

函数身为第一类对象,它可以作为函数的返回值返回,现在我们来考虑如下的例子:

def print_msg():
    # print_msg 是外围函数
    msg = "zen of python"
    def printer():
        # printer 是嵌套函数
        print(msg)
    return printer

another = print_msg()
# 输出 zen of python
another()

这段代码和前面例子的效果完全一样,同样输出 "zen of python"。不同的地方在于内部函数 printer 直接作为返回值返回了。

一般情况下,函数中的局部变量仅在函数的执行期间可用,一旦 print_msg() 执行过后,我们会认为 msg变量将不再可用。然而,在这里我们发现 print_msg 执行完之后,在调用 another 的时候 msg 变量的值正常输出了,这就是闭包的作用,闭包使得局部变量在函数外被访问成为可能。

看完这个例子,我们再来定义闭包,维基百科上的解释是:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。

这里的 another 就是一个闭包,闭包本质上是一个函数,它有两部分组成,printer 函数和变量 msg。闭包使得这些变量的值始终保存在内存中。

闭包,顾名思义,就是一个封闭的包裹,里面包裹着自由变量,就像在类里面定义的属性值一样,自由变量的可见范围随同包裹,哪里可以访问到这个包裹,哪里就可以访问到这个自由变量。

为什么要使用闭包

闭包避免了使用全局变量,此外,闭包允许将函数与其所操作的某些数据(环境)关连起来。这一点与面向对象编程是非常类似的,在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

一般来说,当对象中只有一个方法时,这时使用闭包是更好的选择。来看一个例子:

def adder(x):
    def wrapper(y):
        return x + y
    return wrapper

adder5 = adder(5)
# 输出 15
adder5(10)
# 输出 11
adder5(6)

这比用类来实现更优雅,此外装饰器也是基于闭包的一中应用场景。

所有函数都有一个 __closure__属性,如果这个函数是一个闭包的话,那么它返回的是一个由 cell 对象 组成的元组对象。cell 对象的cell_contents 属性就是闭包中的自由变量。

>>> adder.__closure__
>>> adder5.__closure__
(<cell at 0x103075910: int object at 0x7fd251604518>,)
>>> adder5.__closure__[0].cell_contents
5

这解释了为什么局部变量脱离函数之后,还可以在函数之外被访问的原因的,因为它存储在了闭包的 cell_contents中了。

练习

利用闭包返回一个计数器函数,每次调用它返回递增整数:

def createCounter():
    i = 0
    def counter():
        nonlocal i
        i += 1
        return i
    return counter

# 测试:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
    print('测试通过!')
else:
    print('测试失败!')

 

https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431835236741e42daf5af6514f1a8917b8aaadff31bf000#0

https://foofish.net/python-closure.html

https://segmentfault.com/a/1190000004461404#articleHeader0

 

什么是闭包


官方解释(译文)

Go 函数可以是一个闭包。闭包是一个函数值,它引用了函数体之外的变量。 这个函数可以对这个引用的变量进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。

例如,函数 adder 返回一个闭包。每个返回的闭包都被绑定到其各自的 sum 变量上。

在上面例子中(这里重新贴下代码,和上面代码一样):

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

上面背景高亮部分就是一个闭包,如pos := adder()的adder()表示返回了一个闭包,并赋值给了pos,同时,这个被赋值给了pos的闭包函数被绑定在sum变量上,因此pos闭包函数里的变量sum和neg变量里的sum毫无关系。

Note

func adder() func(int) intfunc(int) int表示adder()的输出值的类型是func(int) int这样一个函数

我对闭包的理解


没有闭包的时候,函数就是一次性买卖,函数执行完毕后就无法再更改函数中变量的值(应该是内存释放了);有了闭包后函数就成为了一个变量的值,只要变量没被释放,函数就会一直处于存活并独享的状态,因此可以后期更改函数中变量的值(因为这样就不会被go给回收内存了,会一直缓存在那里)。

比如,实现一个计算功能:一个数从0开始,每次加上自己的值和当前循环次数(当前第几次,循环从0开始,到9,共10次),然后*2,这样迭代10次:

没有闭包的时候这么写:

func abc(x int) int {
    return x * 2
}

func main() {
    var a int
    for i := 0; i < 10; i ++ {
        a = abc(a+i)
        fmt.Println(a)
    }
}

如果用闭包可以这么写:

func abc() func(int) int {
    res := 0
    return func(x int) int {
        res = (res + x) * 2
        return res
    }
}

func main() {
    a := abc()
    for i := 0; i < 10; i++ {
        fmt.Println(a(i))
    }
}

2种写法输出值都是:

0
2
8
22
52
114
240
494
1004
2026

从上面例子可以看出闭包的3个好处:

  1. 不是一次性消费,被引用声明后可以重复调用,同时变量又只限定在函数里,同时每次调用不是从初始值开始(函数里长期存储变量)

    这有点像使用面向对象的感觉,实例化一个类,这样这个类里的所有方法、属性都是为某个人私有独享的。但比面向对象更加的轻量化

  2. 用了闭包后,主函数就变得简单了,把算法封装在一个函数里,使得主函数省略了a=abc(a+i)这种麻烦事了

  3. 变量污染少,因为如果没用闭包,就会为了传递值到函数里,而在函数外部声明变量,但这样声明的变量又会被下面的其他函数或代码误改。

关于闭包的第一个好处,再啰嗦举个例子

  1. 若不用闭包,则容易对函数外的变量误操作(误操作别人),例:

    var A int = 1
    func main() {
        foo := func () {
            A := 2
            fmt.Println(A)
        }
        foo()
        fmt.Println(A)
    }
    

    输出:

    2
    1
    

    如果手误将A := 2写成了A = 2,那么输出就是:

    2
    2
    

    即会影响外部变量A

  2. 为了将某一个私有的值传递到某个函数里,就需要在函数外声明这个值,但是这样声明会导致这个值在其他函数里也可见了(别人误操作我),例:

    func main() {
        A := 1
        foo := func () int {
            return A + 1
        }
        B := 1
        bar := func () int {
            return B + 2
        }
        fmt.Println(foo())
        fmt.Println(bar())
    }
    

    输出:

    2
    3
    

    在bar里是可以对变量A做操作的,一个不小心就容易误修改变量A

    结论:函数外的变量只能通过参数传递进去,不要通过全局变量的方式的渠道传递进去,当函数内能读取到的变量越多,出错概率(误操作)也就越高。

最后举个例子


实现斐波那契数列:

用闭包:

func fibonacci() func() int {
    b1 := 1
    b2 := 0
    bc := 0
    return func() int {
        bc = b1 + b2
        b1 = b2
        b2 = bc
        return bc
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

输出

1
1
2
3
5
8
13
21
34
55

不用闭包:

func fibonacci(num int) {
    b1 := 1
    b2 := 0
    bc := 0

    for i := 0; i < num; i ++ {
        bc = b1 + b2
        b1 = b2
        b2 = bc
        fmt.Println(bc)
    }
}

func main() {
    fibonacci(10)
}

这样输出也是正确的,但是这么写的话就将循环次数交给了fibonacci函数,即这个函数是个一次性使用的,当函数执行完毕后如果再执行函数,又是从初始值(这里是b1=1)开始,如果想能继续下去,就必须在函数外声明变量,但这样又造成了变量的泛滥(即对其他代码来说这几个变量是毫无意义,还可能造成对这几个变量的误操作),而有的时候想把for循环交由main控制,而是让fibonacci函数完成核心算法、核心数据存储,同时变量又不泛滥给其他代码

不用闭包也可以这么写:

func main() {
    b1 := 1
    b2 := 0
    bc := 0
    fibonacci := func () {
        bc = b1 + b2
        b1 = b2
        b2 = bc
        fmt.Println(bc)
    }
    for i := 0; i < 10; i ++ {
        fibonacci()
    }
}

这样输出结果也是相同的,但是这么写的话,b1、b2、bc就变成了全局都能引用的变量了,而这3个变量其实只在fibonacci里用的到,所以这样把b1、b2、bc给放到全局就显得毫无意义,还有可能对这3个变量误操作

后退

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章