你能解释一下闭包在Python中的作用吗?

101

我一直在阅读与闭包相关的文章,我认为我理解了它们,但是为了不让自己和别人感到困惑,我希望有人能够简洁明了地解释一下闭包。我正在寻找一个简单的解释,可能有助于理解何时何地以及为什么要使用它们。

我已经看了很多关于闭包的资料,对此有了一定的了解,但为了避免混淆自己和他人,我希望有人可以尽可能简明地解释一下闭包。我希望得到一个简单的解释,帮助我理解何时何地以及为什么使用它们。

13个回答

110

闭包之于闭包的解释

对象是带有方法的数据,而闭包是带有数据的函数。

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

7
请注意,nonlocal 是在 Python 3 中添加的,Python 2.x 没有完全实现读写闭包(即您可以读取封闭变量,但无法更改其值)。 - James Porter
8
注:在Python 2中,您可以通过使用可变对象来模拟 nonlocal 关键字,例如 L = [0] \n def counter(): L[0] += 1; return L[0],也就是说,在这种情况下,您不能更改名称(将其绑定到另一个对象),但是您可以更改名称所引用的可变对象本身。必须使用列表,因为在Python中整数是不可变的。 - jfs
1
@J.F.Sebastian:没错,但总是感觉像是一个肮脏的、不正当的技巧:) - James Porter
"counter() is a closure" 应该改为 "counter是一个闭包" 吗? - Jason Law
@JasonLaw 正确(如果将其解释为函数调用,则只得到 int)。在注释中(使用普通英语),我使用了一个约定,即括号前的名称指的是函数(以区别于没有 `` markdown 或类似语法的其他单词)。 - jfs

48

很简单:一个函数引用来自包含作用域的变量,可能是在控制流离开该作用域之后。最后这一部分非常有用:

>>> 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
请注意,在函数f和g内部,12和4已经被“隐藏”,这个特性使得f和g成为适当的闭包。

4
不需要执行constant = x这一段代码,你可以在嵌套函数中直接使用return y + x(或者使用参数名为constant的参数),它完全可以正常工作;参数和非参数本地变量一样被闭包捕获。 - ShadowRanger

23

说实话,除了我从来都不清楚什么是“闭包”以及为什么它被称作“闭包”,我对闭包的理解非常好。我建议你放弃寻找术语背后的逻辑。

无论如何,下面是我的解释:

def foo():
   x = 3
   def bar():
      print x
   x = 5
   return bar

bar = foo()
bar()   # print 5

这里的一个关键思想是,从foo返回的函数对象保留了与本地变量'x'的关联,即使'x'已经超出范围并且应该不存在了。这个钩子是指变量本身,而不仅仅是该变量在那个时间点上所具有的值,因此当调用bar时,它会打印5而不是3。

此外,请明确Python 2.x的闭包受到限制:我无法在'bar'中修改'x',因为编写'x = bla'将声明bar中的本地'x',而不是赋值给foo中的'x'。这是Python赋值=声明的副作用。为了解决这个问题,Python 3.0引入了nonlocal关键字:

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

16

我喜欢这个简明扼要的定义:

一个函数可以引用不再活动的环境。

我会补充一下

闭包允许您将变量绑定到一个函数中,而不需要将它们作为参数传递。

接受参数的装饰器是闭包的常见用途。闭包是这种“函数工厂”的常见实现机制。当策略在运行时通过数据进行修改时,我经常选择在策略模式中使用闭包。

在允许匿名块定义的语言中--例如Ruby、C#--闭包可用于实现(相当于)新颖的控制结构。匿名块的缺乏是Python中闭包的限制之一


8
我从未听说过在解释闭包的同时使用事务,并且这里确实没有任何事务语义。它被称为闭包,因为它“封闭”了外部变量(常量)——也就是说,它不仅仅是一个函数,而是创建该函数的环境的封闭。在下面的示例中,更改x后调用闭包g也将更改g内x的值,因为g封闭了x:
x = 0

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


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

另外,就目前而言,g()计算的是x * 2,但没有返回任何值。应该是return x * 2。尽管如此,对于“闭包”一词的解释还是要点赞的。 - Bruno Le Floch

6
# 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. 封闭函数必须返回嵌套函数。

# 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

3
在Python中,闭包是一个函数实例,其变量被不可变地绑定。实际上,数据模型 在函数的 __closure__ 属性的描述中解释了这一点:

为函数的自由变量包含绑定的 None 或元组。只读

为了证明这一点:
def enclosure(foo):
    def closure(bar):
        print(foo, bar)
    return closure

closure_instance = enclosure('foo')

显然,我们知道现在有一个函数指向了变量名为closure_instance。假设,如果我们用一个对象bar来调用它,它应该打印出字符串'foo'bar的字符串表示形式。
事实上,字符串'foo'被绑定到该函数的实例上,我们可以通过访问__closure__属性的元组中的第一个(也是唯一一个)单元格的cell_contents属性来直接读取它。
>>> closure_instance.__closure__[0].cell_contents
'foo'

作为旁注,单元格对象在 C API 文档中有描述:
“Cell” 对象用于实现被多个作用域引用的变量
我们可以演示闭包的使用,注意到 'foo' 被固定在函数中不会改变:
>>> closure_instance('bar')
foo bar
>>> closure_instance('baz')
foo baz
>>> closure_instance('quux')
foo quux

"什么都无法改变它:"
>>> closure_instance.__closure__ = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: readonly attribute

部分函数

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

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

还有更复杂的闭包,不适用于部分函数示例,如果时间允许,我将进一步演示它们。

3
这里是闭包的一个典型应用场景 - 用于GUI元素的回调函数(这是子类化按钮类的另一种选择)。例如,您可以构造一个函数,该函数将在响应按钮按下时被调用,并"闭合"父范围中与处理点击所必需的相关变量。这样,您可以从同一初始化函数连接相当复杂的界面,将所有依赖项构建到闭包中。

1
我们都在Python中使用过装饰器。它们是展示Python中闭包函数的好例子。
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
我想分享一个有关闭包的例子和解释。我制作了一个Python示例,并绘制了两个图表来演示堆栈状态。
def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('\n’ * margin_top, a * n, 
            ' ‘ * padding, msg, ' ‘ * padding, b * n)
    return message

f = maker('*', '#', 5)
g = maker('', '♥’, 3)
…
f('hello')
g(‘good bye!')

这段代码的输出如下:
*****      hello      #####

      good bye!    ♥♥♥

这里有两个图示,展示了堆栈和附加到函数对象的闭包。 当函数从制造者返回时 当函数稍后被调用时 当函数通过参数或非本地变量调用时,代码需要局部变量绑定,如margin_top、padding以及a、b、n。为了确保函数代码正常工作,maker函数的堆栈帧,早已消失,应该是可访问的,这被备份在我们可以找到的'消息'函数对象的闭包中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接