关于函数式编程:你能解释一下闭包(因为它们与Python有关)吗?

Can you explain closures (as they relate to Python)?

我已经读了很多关于闭包的文章,我想我理解它们,但是我希望有人能尽可能简洁、清晰地解释闭包,而不会给自己和其他人的印象蒙上阴影。我正在寻找一个简单的解释,可以帮助我理解在哪里和为什么我要使用它们。


关闭时关闭

Objects are data with methods
attached, closures are functions with
data attached.

1
2
3
4
5
6
7
8
9
10
11
12
13
def make_counter():
    i = 0
    def counter(): # counter() is a closure
        nonlocal i
        i += 1
        return i
    return counter

c1 = make_counter()
c2 = make_counter()

print (c1(), c1(), c2(), c2())
# -> 1 2 1 2


这很简单:一个引用包含范围中变量的函数,可能是在控制流离开该范围之后。最后一位非常有用:

1
2
3
4
5
6
7
8
9
10
11
12
>>> def makeConstantAdder(x):
...     constant = x
...     def adder(y):
...         return y + constant
...     return adder
...
>>> f = makeConstantAdder(12)
>>> f(3)
15
>>> g = makeConstantAdder(4)
>>> g(3)
7

请注意,12和4分别在f和g中"消失",这一特性使f和g正确关闭。


老实说,我完全理解闭包,只是我从来没有弄清楚什么是"闭包",什么是"闭包"。我建议你放弃寻找术语选择背后的逻辑。

不管怎样,我的解释是:

1
2
3
4
5
6
7
8
9
def foo():
   x = 3
   def bar():
      print x
   x = 5
   return bar

bar = foo()
bar()   # print 5

这里的一个关键思想是,从foo返回的函数对象保留了对局部变量"x"的一个钩子,即使"x"已经超出范围,应该是无效的。这个钩子指向var本身,而不仅仅是var当时的值,所以当调用bar时,它打印5而不是3。

同样要清楚的是,python 2.x的闭包是有限的:我无法在"bar"中修改"x",因为写入"x=bla"将在bar中声明一个本地的"x",而不是分配给foo的"x"。这是python的assignment=声明的副作用。为了解决这个问题,python 3.0引入了非本地关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
def foo():
   x = 3
   def bar():
      print x
   def ack():
      nonlocal x
      x = 7
   x = 5
   return (bar, ack)

bar, ack = foo()
ack()   # modify x of the call to foo
bar()   # print 7

我喜欢这个粗略、简洁的定义:

A function that can refer to environments that are no longer active.

我会添加

A closure allows you to bind variables into a function without passing them as parameters.

接受参数的修饰符是闭包的常见用法。闭包是那种"函数工厂"的一种常见实现机制。当策略在运行时被数据修改时,我经常选择在策略模式中使用闭包。

在一种允许匿名块定义的语言中——例如Ruby、C——闭包可以用来实现(多少)新的控制结构。缺少匿名块是Python中闭包的限制之一。


我从来没有听说过事务与解释什么是闭包在同一个上下文中使用,这里实际上没有任何事务语义。

它之所以被称为闭包,是因为它"关闭"了外部变量(常量),也就是说,它不仅是一个函数,而且是创建函数的环境的一个外壳。

在下面的示例中,更改x后调用闭包g也将更改g中x的值,因为g关闭x:

1
2
3
4
5
6
7
8
9
10
11
12
x = 0

def f():
    def g():
        return x * 2
    return g


closure = f()
print(closure()) # 0
x = 2
print(closure()) # 4


这里是一个典型的闭包用例——对GUI元素的回调(这是对button类进行子类化的一种替代方法)。例如,您可以构造一个函数,该函数将在按钮按下时被调用,并"关闭"父作用域中处理单击所必需的相关变量。这样,您就可以从相同的初始化函数连接非常复杂的接口,将所有依赖项构建到闭包中。


下面是python3闭包的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def closure(x):
    def counter():
        nonlocal x
        x += 1
        return x
    return counter;

counter1 = closure(100);
counter2 = closure(200);

print("i from closure 1" + str(counter1()))
print("i from closure 1" + str(counter1()))
print("i from closure 2" + str(counter2()))
print("i from closure 1" + str(counter1()))
print("i from closure 1" + str(counter1()))
print("i from closure 1" + str(counter1()))
print("i from closure 2" + str(counter2()))

# result

i from closure 1 101
i from closure 1 102
i from closure 2 201
i from closure 1 103
i from closure 1 104
i from closure 1 105
i from closure 2 202

在Python中,闭包是一个函数的实例,该函数的变量是固定绑定的。

事实上,数据模型在其对函数的__closure__属性的描述中解释了这一点:

None or a tuple of cells that contain bindings for the function’s free variables. Read-only

为了证明这一点:

1
2
3
4
5
6
def enclosure(foo):
    def closure(bar):
        print(foo, bar)
    return closure

closure_instance = enclosure('foo')

很明显,我们知道现在有一个函数指向变量名closure_instance。表面上,如果我们用对象bar来调用它,它应该打印字符串'foo',以及bar的字符串表示形式。

实际上,字符串"foo"与函数的实例绑定在一起,我们可以通过访问__closure__属性的元组中第一个(并且只有一个)单元格的cell_contents属性,在这里直接读取它:

1
2
>>> closure_instance.__closure__[0].cell_contents
'foo'

顺便提一句,cell对象在C API文档中进行了描述:

"Cell" objects are used to implement variables referenced by multiple
scopes

我们可以演示闭包的用法,注意到'foo'卡在函数中,不会改变:

1
2
3
4
5
6
>>> closure_instance('bar')
foo bar
>>> closure_instance('baz')
foo baz
>>> closure_instance('quux')
foo quux

没有什么可以改变它:

1
2
3
4
>>> closure_instance.__closure__ = None
Traceback (most recent call last):
  File"<stdin>", line 1, in <module>
TypeError: readonly attribute

部分函数

给出的示例使用闭包作为部分函数,但是如果这是我们唯一的目标,那么使用functools.partial可以实现相同的目标。

1
2
3
4
5
6
7
8
>>> from __future__ import print_function # use this if you're in Python 2.
>>> partial_function = functools.partial(print, 'foo')
>>> partial_function('bar')
foo bar
>>> partial_function('baz')
foo baz
>>> partial_function('quux')
foo quux

还有一些更复杂的闭包不适合部分函数示例,我将在时间允许的情况下进一步演示它们。


我想分享我的例子和一个关于闭包的解释。我做了一个Python示例,并用两个数字来演示堆栈状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('
’ * margin_top, a * n,
            '
‘ * padding, msg, ' ‘ * padding, b * n)
    return message

f = maker('
*', '#', 5)
g = maker('?', '?’, 3)

f('
hello')
g(‘good bye!'
)

此代码的输出如下:

1
2
3
*****      hello      #####

???      good bye!    ???

这里有两个图显示堆栈和附加到函数对象的闭包。

当函数从maker返回时

稍后调用函数时

当通过参数或非局部变量调用函数时,代码需要局部变量绑定,如Margin_Top、Padding以及a、b、n。为了确保函数代码正常工作,很久以前就不存在的maker函数的堆栈框架应该是可访问的,它备份在闭包中,我们可以通过h'消息的函数对象。


我们都在python中使用了装饰器。这些示例很好地展示了什么是Python中的闭包函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test():
    def decorator(func):
        def wrapper(*args):
            b = args[1] + 5
            return func(b)
        return wrapper

@decorator
def foo(val):
    print val + 2

obj = Test()
obj.foo(5)

这里的最终值是12

在这里,包装器函数能够访问func对象,因为包装器是"词汇闭包",它可以访问它的父属性。这就是为什么,它能够访问func对象。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# A Closure is a function object that remembers values in enclosing scopes even if they are not present in memory.

# Defining a closure

# This is an outer function.
def outer_function(message):
    # This is an inner nested function.
    def inner_function():
        print(message)
    return inner_function

# Now lets call the outer function and return value bound to name 'temp'
temp = outer_function("Hello")
# On calling temp, 'message' will be still be remembered although we had finished executing outer_function()
temp()
# Technique by which some data('message') that remembers values in enclosing scopes
# even if they are not present in memory is called closures

# Output: Hello

关闭要满足的标准是:

  • 我们必须有嵌套函数。
  • 嵌套函数必须引用封闭函数中定义的值。
  • 封闭函数必须返回嵌套函数。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # Example 2
    def make_multiplier_of(n): # Outer function
        def multiplier(x): # Inner nested function
            return x * n
        return multiplier
    # Multiplier of 3
    times3 = make_multiplier_of(3)
    # Multiplier of 5
    times5 = make_multiplier_of(5)
    print(times5(3)) # 15
    print(times3(2)) #  6


    对于我来说,"闭包"是能够记住创建环境的函数。此功能允许您在闭包wich中使用变量或方法,换句话说,您将无法使用它们,因为它们不再存在,或者由于作用域而无法访问。让我们看看Ruby中的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def makefunction (x)
      def multiply (a,b)
        puts a*b
      end
      return lambda {|n| multiply(n,x)} # => returning a closure
    end

    func = makefunction(2) # => we capture the closure
    func.call(6)    # => Result equal"12"

    即使"乘法"方法和"x"变量都不存在,它也能工作。所有这些都是因为关闭功能需要记住。


    我见过的最好的解释就是解释这个机制。就像这样:

    将程序堆栈想象为一个退化树,其中每个节点只有一个子节点,而单个叶节点是当前执行过程的上下文。

    现在放松约束,每个节点只能有一个子节点。

    如果这样做,您可以有一个构造("yield"),它可以从过程返回而不丢弃本地上下文(即返回时它不会从堆栈中弹出)。下一次调用过程时,调用会拾取旧的堆栈(树)帧,并在停止的地方继续执行。