返回

闭包到底能做啥?它有着什么样的妙用?

前端

闭包(Closure)是函数式编程中一个非常重要且别具一格的概念,在计算机科学中备受推崇。它能提供一个稳定且长期的环境,确保栈 式数据在函数多次执行时不会丢失。因此,能够理解闭包的特性、优点与缺点,对掌握计算机科学至关重要。接下来,我们将共同探索闭包的妙用,深入理解这种奇妙的编程特性。

闭包的妙处:让栈式数据得以延续

谈及闭包,首先我们有必要理清楚栈式数据和函数式编程的脉络。

栈 式数据和函数式编程是计算机编程的基石。栈 式数据的本质是函数的局部数据,是函数自身的private。换言之,其生命周期仅限于所归属的函数。而一旦函数的执行结束,这些栈式数据也随之烟消云散。函数 式编程,则是以高阶函数、闭包和递归等技术为基石的编程方法。由于栈式数据不可复用,因此函数 式编程在项目执行中显得尤为重要,可以说构成了项目执行的灵魂所在。

闭包的概念,最早源自20世纪70年代。闭包的本质是:一个函数及其代码在运行时的环境一起构成一个闭包。也即是:内层函数可以引用到外层函数中的任何东西,包括函数、数据。这种特性,在函数 式编程中能派上很大用场,因为,依赖于它,函数就能够将数据保留至下次执行时再行使用。如此,我们便得以延续栈 式数据,使它们得以在下次函数运行时继续保持其原有状态。为此,我们可以来举个例子。假设,我们定义了一个顶层函数,名之为: calculate() 。我们设定: calculate() 被设计用来向列表追加一个数字,并将改增后的列表作为输出。

def calculate(initial_list):
  """计算列表的长度。

  Args:
    initial_list: 用以计算长度的列表。

  Returns:
    改增后列表的长度。
  """
  # 计算初始长度并将其追加到列表
  length = len(initial_list)
  initial_list.append(length)

  # 返回改增后的列表长度
  return length

倘若把 calculate() 仅仅作为一个常规函数看待,那么,毫无疑问, length 会随着函数的执行而随之逝去。这样,我们便无 法得知所计算出的长度。然,依托于闭包,我们就能够将 length 保留于下次执行时再行使用,这一过程,我们能够通过设定内层函数来实现。

def calculate(initial_list):
  """计算列表的长度。

  Args:
    initial_list: 用以计算长度的列表。

  Returns:
    改增后列表的长度。
  """

  # 定义一个闭包
  def calculate_and_append(x):
    """将列表的长度作为后缀追加到列表。

    Args:
      x: 列表的某个元素

    Returns:
      改增后的列表。
    """
    # 确保`calculate_and_append`能使用 `initial_list`
    nonlocal initial_list

    length = len(initial_list)
    initial_list.append(length)
    return x

  # 返回具有闭包的 `calculate_and_append`
  return calculate_and_append

那么,现在,我们能够创建一个通过这个闭包返回的 calculate_and_append() 来生成一个元素列表。

calculate = calculate([])

现在,我们便可以反复使用 calculate_and_append() 函数来增补 initial_list 的长度。

calculate_and_append(1)  # [0, 1]
calculate_and_append(3)  # [0, 1, 2, 3]
calculate_and_append(1)  # [0, 1, 2, 3, 4]

如你所见,我们现在得以反复使用 calculate_and_append() 以长度为后缀不断扩充 initial_list 。这展示了闭包能帮助我们如何得以保留 length ,即便在函数 calculate() 多次执行时, length 仍能保持原有的值,这便得益于 calculate_and_append() 函数对 initial_list 的修改。

实际上,这只是闭包的妙用之一。闭包还有诸多其他妙处。利用闭包,我们能够:

  • 确保在不同的函数执行周期中始终保持一致的栈 式数据。
  • 充分利用函数 式编程的特性。
  • 保留函数原有的代码,即便该函数执行已久,进而得以执行新内容。
  • 剔除对 length 等局部 private 的硬编码依赖,进而使函数代码更加精简易读。

得益于闭包,我们能够将局部 private ,即栈式数据,保留在函数执行的不同周期中,以便日后使用。可以说,闭包是计算机科学的一项必备技术。

闭包的妙用不止于此,倘若我们想要在函数多次执行时保持一组私有数据的稳定状态,我们就不得不依赖于闭包。例如,当我们设计一个利用 calculate() 来计算一组数字列表长度的函数,那么,每当 calculate() 被执行,我们都能够利用 calculate_and_append() 来保留 length 以备日后使用。如此,我们就不必将 length 硬编码至函数之中,进而将 calculate() 简化为如下形式:

def calculate(initial_list):
  """计算列表长度。

  Args:
    initial_list: 用于计算长度的列表。

  Returns:
    改增后列表的长度。
  """
  length = len(initial_list)
  return length

得益于此,我们可以通过闭包来保持一些数据的可变状态,进而减少对 length 进行硬编码依赖的函数数量。

Python中的闭包

在 Python 中,我们可以用 lambda 函数来打造闭包。 lambda 函数可以利用简洁的语法来定义函数。如我们所举例的 calculate() ,在 Python 中,我们可以用 lambda 函数以如下方式实现:

calculate = lambda initial_list: len(initial_list)

而在 Python 中,利用内层 lambda 函数来定义 calculate_and_append() ,则需要如下操作:

calculate = lambda initial_list: lambda x: len(initial_list) + x

由此,我们得以剔除函数式的限定,实现了类似于 calculate_and_append() 这类只专注于参数 x 的函数。

calculate = lambda x: len(initial_list) + x

现在,我们便能够以如下方式返回 calculate() 函数:

calculate = calculate([])

calculate() 函数在 Python 中的具体应用如下:

calculate(3)  # 4
calculate(1)  # 5
calculate(5)  # 6

如此,你便得以多次执行 calculate() 函数,不断获取不断扩大的列表长度。

不当闭包使用的负面

当我们讨论完闭包的诸多妙用时,自然也需要了解不当使用闭包可能导致的不良后果。

例如,利用闭包来赋存一些预期用于一次执行,而不是长期保持的栈 式数据,这极可能导致我们耗费宝贵的计算资源来保持这些数据的稳定。而且,赋存这些数据,还可能损害我们控制栈溢出的能力。栈溢出时函数在运行中消耗栈 式数据而堆积的速度远高于释放这些数据的速度。当栈溢出发生时,函数就无法继续执行。闭包若使用不当,很可能导致栈溢出的问题。

由此,我们不应利用闭包来赋存那些预期用于一次执行的栈 式数据。换言之,应确保仅赋存预期长期保留的栈 式数据。这种做法,将极大程度地削减栈溢出的危害,同时节约宝贵的计算资源,进而提升我们对栈溢出的控制能力。

闭包的不当使用,往往会给人代码运行极其缓慢的假象。导致这种情况的罪魁祸首,往往是对于栈溢出的错误把控。栈溢出时函数在运行中消耗栈 式数据