7.2 被解放的函数

1.函数作为参数和返回值

在函数式编程中,函数是第一级对象。所谓“第一级对象”,即函数能像普通对象一样使用。因此,函数的使用变得更加自由。对于“一切皆对象”的Python来说,这是自然而然的结果。既然如此,那么函数可以像一个普通对象一样,成为其他函数的参数。比如下面的程序,函数就充当了参数:


def square_sum(a, b):
    return a**2 + b**2

def cubic_sum(a, b):
    return a**3 + b**3

def argument_demo(f, a, b):
    return f(a, b)

print(argument_demo(square_sum, 3, 5))        # 打印34
print(argument_demo(cubic_sum, 3, 5))         # 打印152

函数argument_demo()的第一个参数f就是一个函数对象。按照位置传参,square_sum()传递给函数argument_demo(),对应参数列表中的f。f会在argument_demo()中被调用。我们可以把其他函数,如cubic_sum()作为参数传递给argument_demo()。

很多语言都能把函数作为参数使用,例如C语言。在图形化界面编程时,这样一个作为参数的函数经常起到回调(Callback)的作用。当某个事件发生时,比如界面上的一个按钮被按下,回调函数就会被调用。下面是一个GUI回调的例子:


import tkinter as tk

def callback():
    """
    callback function for button click
    """
    listbox.insert(tk.END, "Hello World!")

if __name__ == "__main__":
    master = tk.Tk()

    button = tk.Button(master, text="OK", command=callback)
    button.pack()

    listbox = tk.Listbox(master)
    listbox.pack()

    tk.mainloop()

Python中内置了tkinter的图形化功能。在上面的程序中,回调函数将在列表栏中插入"Hello World!"。回调函数作为参数传给按钮的构造器。每当按钮被点击时,回调函数就会被调用,如图7-3所示。

图7-3 回调函数运行结果

2.函数作为返回值

既然函数是一个对象,那么它就可以成为另一个函数的返回结果。


def line_conf():
    def line(x):
        return 2*x+1
    return line       # return a function object

my_line = line_conf()
print(my_line(5))         # 打印11

上面的代码可以成功运行。line_conf()的返回结果被赋给line对象。上面的代码将打印11。

在上面的例子中,我们看到了在一个函数内部定义的函数。和函数内部的对象一样,函数对象也有存活范围,也就是函数对象的作用域。Python的缩进形式很容易让我们看到函数对象的作用域。函数对象的作用域与它的def的缩进层级相同。比如下面的代码,我们在line_conf()函数的隶属范围内定义的函数line(),就只能在line_conf()的隶属范围内调用。


def line_conf():
    def line(x):
        return 2*x+1
    print(line(5))       #作用域内

if __name__=="__main__":
    line_conf()
    print(line(5))       #作用域外,报错

函数line()定义了一条直线(y = 2x + 1)。可以看到,在line_conf()中可以调用line()函数,而在作用域之外调用line()函数将会有下面的错误:


NameError: name 'line' is not defined

说明这已经超出了函数line()的作用域。Python对该函数的调用失败。

3.闭包

上面函数中,line()定义嵌套在另一个函数内部。如果函数的定义中引用了外部变量,会发生什么呢?


def line_conf():
    b = 15
    def line(x):
        return 2*x+b
    b = 5
    return line                   #返回函数对象

if __name__ == "__main__":
    my_line = line_conf()
    print(my_line(5))             # 打印15

可以看到,line()定义的隶属程序块中引用了高层级的变量b。b的定义并不在line()的内部,而是一个外部对象。我们称b为line()的环境变量。尽管b位于line()定义的外部,但当line被函数line_conf()返回时,还是会带有 b的信息。

一个函数和它的环境变量合在一起,就构成了一个闭包(Closure)。上面程序中,b分别在line()定义的前后有两次不同的赋值。上面的代码将打印15,也就是说,line()参照的是值为5的b值。因此,闭包中包含的是内部函数返回时的外部对象的值。

在Python中,所谓的闭包是一个包含有环境变量取值的函数对象。环境变量取值被复制到函数对象的__closure__属性中。比如下面的代码:


def line_conf():
    b = 15

    def line(x):
        return 2*x+b
    b = 5
    return line       # 返回函数对象

if __name__ == "__main__":
    my_line = line_conf()
    print(my_line.__closure__)
    print(my_line.__closure__[0].cell_contents)

可以看到,my_line()的__closure__属性中包含了一个元组。这个元组中的每个元素都是cell类型的对象。第一个cell包含的就是整数5,也就是我们返回闭包时的环境变量b的取值。

闭包可以提高代码的可复用性。我们看下面三个函数:


def line1(x):
    return x + 1

def line2(x):
    return 4*x + 1

def line3(x):
    return 5*x + 10

def line4(x):
    return -2*x – 6

如果把上面的程序改为闭包,那么代码就会简单很多:


def line_conf(a, b):
    def line(x):
     return a*x + b
return line

line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
line3 = line_conf(5, 10)
line4 = line_conf(-2, -6)

这个例子中,函数line()与环境变量 a、b 构成闭包。在创建闭包的时候,我们通过line_conf()的参数a、b说明直线的参量。这样,我们就能复用同一个闭包,通过代入不同的数据来获得不同的直线函数,如 y = x + 1和 y = 4x + 5。闭包实际上创建了一群形式相似的函数。

除了复用代码,闭包还能起到减少函数参数的作用:


def curve_closure(a, b, c):
    def curve(x):
        return a*x**2 + b*x + c
    return curve

curve1 = curve_closure(1, 2, 1)

函数curve()是一个二次函数。它除了自变量 x 外,还有a、b、c三个参数。通过curve_closure()这个闭包,我们可以预设a、b、c三个参数的值。从而起到减参的效果。

闭包的减参作用对于并行运算来说很有意义。在并行运算的环境下,我们可以让每台电脑负责一个函数,把上一台电脑的输出和下一台电脑的输入串联起来。最终,我们像流水线一样工作,从串联的电脑集群一端输入数据,从另一端输出数据。由于每台电脑只能接收一个输入,所以在串联之前,必须用闭包之类的办法把参数的个数降为1。

results matching ""

    No results matching ""