闭包

维基百科:在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。

认识闭包

词源

彼得·兰丁 在1964年将术语 闭包 定义为一种包含 环境成分 和 控制成分的实体,用于在他的SECD 机器上对表达式求值。Joel Moses 认为是 Landin 发明了 闭包 这一术语,用来指代某些其开放绑定(自由变量)已经由其语法环境完成闭合(或者绑定)的 lambda 表达式,从而形成了闭合的表达式,或称闭包。这一用法后来于 1975 年被 Sussman 和 Steele 在定义 Scheme 语言的时候予以采纳。并广为流传。(维基百科)

概念

闭包这一术语,用来指代某些其开放绑定(自由变量)已经由其语法环境完成闭合(或者绑定)的 lambda 表达式,从而形成了闭合的表达式,或称闭包。

“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。

“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

用途

  1. 因为闭包只有在被调用时才执行操作,即“惰性求值”,所以它可以被用来定义控制结构。例如:在Smalltalk语言中,所有的控制结构,包括分歧条件(if/then/else)和循环(while和for),都是通过闭包实现的。用户也可以使用闭包定义自己的控制结构。

  2. 多个函数可以使用一个相同的环境,这使得它们可以通过改变那个环境相互交流。

实现

典型实现方式是定义一个特殊的数据结构,保存了函数地址指针与闭包创建时的函数的词法环境表示(那些非局部变量的绑定)。使用函数调用栈的语言实现闭包比较困难,因而这也说明了为什么大多数实现闭包的语言是基于垃圾收集机制——当然,不使用垃圾收集也可以做到。 闭包的实现与函数对象很相似。通过将自由变量放进参数表、并扩大函数名字的作用域,可以把一个闭包 / 匿名 / 内部函数变成一个普通的函数,这叫做“Lambda 提升”。

Python闭包

在Python中,函数本身也是对象

和list,tuple等创建的对象一样,当你定义一个函数时,函数也是对象:

def func(a, b):
	return a + b

函数对象被变量func引用着,它接受两个参数a和b,计算这两个参数的和作为返回值。 因此,你完全可以用其他变量名引用这个函数对象:

add = func

print(add(1, 2))

闭包举例: hello.py

import world

filename = 'hello.py'

def call_func(f):
    return f()

if __name__ == '__main__':
    print(call_func(world.show_filename))  #打印:filename: world.py

world.py

filename = 'world.py'

def show_filename():
    return 'filename: %s' % filename

show_filename函数使用的filename变量值是与在它相同环境(world.py模块)中定义的那个。 对于嵌套函数,这一机制则会表现得更加明显:闭包将会捕捉内层函数执行所需的整个环境。

#enclosed.py
import hello
def wrapper():
    filename = 'enclosed.py'
    def show_filename():
        return 'filename: %s' % filename
    print(hello.call_func(show_filename))  #打印:filename: enclosed.py
    print(show_filename.__globals__)

show_filename函数的_globals_

{'__doc__': None, 
'__name__': '__main__', 
'hello': <module 'hello' from '/home/lewis/桌面/pyexercise/Closure/hello.py'>, 
'__builtins__': <module 'builtins' (built-in)>, 
'__cached__': None, 
'__package__': None, 
'__file__': 'enclosed.py', 
'__spec__': None, 
'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f854a0c68d0>, 
'wrapper': <function wrapper at 0x7f854a0fff28>}

“_builtins_”:内建作用域环境 “wrapper”:直接外围环境 “hello”:全局环境

当代码执行到show_filename中的return 'filename: %s' % filename语句时,解析器按照下面的顺序查找filename变量:

  1. Local-本地函数(show_filename)内部,通过任何方式赋值的,而且没有被global关键字声明为全局变量的filename变量;

  2. Enclosing-直接外围空间(上层函数wrapper)的本地作用域,查找filename变量(如果有多层嵌套,则由内而外逐层查找,直至最外层的函数);

  3. Global-全局空间(模块enclosed.py),在模块顶层赋值的filename变量;

  4. Builtin-内置模块(_builtin_)中预定义的变量名中查找filename变量。

在任何一层先找到了符合要求的filename变量,则不再向更外层查找。如果直到Builtin层仍然没有找到符合要求的变量,则抛出NameError异常。这就是变量解析的:LEGB法则

装饰器

def checkParams(func):
    def wrapper(a, b):
        if isinstance(a, (int, float)) and isinstance(b, (int, float)):
            return func(a, b)
    return wrapper

@checkParams
def add(a, b):
    return a + b
  • @checkParams相当于add = checkParams(add),参数func将成为一个本地引用,指向函数对象add;

  • 在checkParams内部,我们定义了一个wrapper函数,添加了参数类型检查的功能,然后调用func(a, b),根据LEGB法则,解释器将搜索几个作用域,并最终在(Enclosing层)checkParams函数的本地作用域中找到func;

  • 最后的return wrapper,这将创建一个闭包,func变量(add函数对象的一个引用)将会封存在闭包的执行环境中,不会随着checkParams的返回而被回收。

Java闭包

Java中对闭包的支持可以用 内部类(内部类能够访问外部类的所有属性及方法)+接口 演示:

public class Milk {

	public final static String name = "milk";
	public static int num = 24;
	
	/**
	 * 闭包
	 * @return 返回一个喝牛奶动作
	 * 变量name,num绑定到函数drink()上
	 */
	public Action drinkMilk() {
		return new Action() {
			@Override
			public void drink() {
				if(num == 0) {
					System.out.println("喝完啦");
				} else {
					num--;
					System.out.println("喝掉一瓶牛奶");
				}
			}
		};
	}
	
	public void currentNum() {
		System.out.println(name+ "剩余:" + num);
	}
}

interface Action {
	void drink();
}

JavaScript闭包

function lazy_num(arr) {
	var sum = function() {
		return arr.reduce(function(x, y) {
			return x + y;
		});
	};
	return sum;
}

var f = lazy_sum([1, 2, 3, 4, 5]);	//function sum()
f();	// 15

在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量(如arr),当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中。

注意:

function count() {
	var arr = [];
	for(var i=1; i<=3; i++) {
		arr.push(function() {
			return i * i;
		});
	}
	return arr;
}

var results = count();
var f1 = results[0];
var f1 = results[1];
var f1 = results[2];

f1();	//16
f2();	//16
f3();	//16

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


闭包 (计算机科学) - 维基百科,自由的百科全书 Python 里为什么函数可以返回一个函数内部定义的函数? - 知乎 深入理解Java闭包概念 闭包 - 廖雪峰的官方网站

Last updated