分享

关于Python闭包你了解多少?

 网摘文苑 2022-11-19 发布于新疆

闭包并不是 Python 独有的概念。许多其他编程语言共享相同的概念。虽然很多初学者可能听说过它,但他们并不确切知道它是什么以及如何使用它。在本文中,我重点介绍了有关 Python 闭包的最基本知识,希望你能更好地理解这个概念。

一、内外功能

如前所述,闭包存在于许多编程语言中,以下定义取自百度百科:

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁

这个定义对很多人来说太技术化了,而且它并不特定于 Python。为了帮助你理解这个概念,这里有一个简单的版本:闭包是在外部函数中创建并使用外部函数变量的内部函数。它由外部函数作为其输出值返回。还是太技术了?我认同。最好通过一个真实的例子来理解。我们先看下面的代码片段:

def multiplier_creator(n): def multiplier(number): return number * n return multiplierdouble_multiplier = multiplier_creator(2)triple_multiplier = multiplier_creator(3)

在上面的代码中,double_multiplier和triple_multiplier是两个闭包,因为它们满足闭包的定义,如下所述。

  1. 是在函数内创建的multipler内部multiplier_creator函数,由于它在函数外部,因此称为外部函数multiplier。内部函数通常被称为嵌套函数,因为它嵌套在另一个函数中。

  2. 创建的内部函数是外部函数的返回值。需要注意的是,函数直接multiplier_creator返回multiplier函数,而不是函数的输出值multiplier。

  3. 内部函数multipler使用n变量,它是外部函数的参数multiplier_creator。内部函数对外部变量的访问也称为非局部变量绑定。

非局部变量的绑定可能是关于闭包最令人困惑的部分。让我们在下一节中探讨它。

2.局部变量和非局部变量

当我们使用函数时,我们知道在函数中,我们可以自由使用任何传递的参数,这些参数称为局部变量。本质上,函数形成了一个局部作用域,它限制了对其变量的访问。

就上面的例子而言,multiplier_creator函数定义了一个局部作用域,参数n是一个局部变量。在类似的函数中,内部函数multiplier定义了另一个局部作用域,参数number是局部变量。但值得注意的是,该multipler函数还使用参数n。虽然n是multiplier_creator的作用域的局部变量,但它不是multipler函数的局部变量。因此,在示例中,乘数函数使用非局部变量,也称为自由变量。

除了局部变量和非局部变量的区别,你们中的一些人可能还听说过全局变量,它们是在模块级别定义的变量。

你可能已经注意到,从内部函数的角度来看,我们将访问外部函数的局部变量的过程称为非局部变量绑定。用绑定来描述这个特性是很有意义的。绑定到底是什么意思?让我们在下一节中学习它。

3. 非局部变量绑定

非局部变量的绑定在其他一些语言中也称为非局部变量捕获,以描述闭包的特性。你可以简单地将其概念化为内部函数“拥有”使用的非局部变量。让我们观察以下特征:

>>> del multiplier_creator >>> double_multiplier(5) 10 >>> triple_multiplier(5) 15

在上面的代码中,我们删除了外部函数multiplier_creator,这样这个函数就变得不可访问了。但请注意闭包(即double_multiplier和triple_multiplier)使用multiplier_creator的参数n。因此,有些人可能认为关闭将停止工作。然而,他们仍在产生预期的结果。

根本原因是闭包已经建立了它使用的非局部变量的绑定。换句话说,它有一个绑定变量的副本。为了观察这个特性,考虑一些特殊的检查函数,如下所示:

>>> double_multiplier.__code__.co_freevars('n',)>>> double_multiplier.__closure__[0].cell_contents2>>> triple_multiplier.__code__.co_freevars('n',)>>> triple_multiplier.__closure__[0].cell_contents3

__code__.co_freevars允许我们检查闭包的非局部变量绑定的名称,允许__closure__[0].cell_contents我们检查绑定的非局部变量的值。你不必知道这些功能的细节,它们只是底层实现。

重要的是要知道,因为闭包通过绑定拥有自己的非局部变量副本,所以即使外部函数已被删除,它们仍然可以工作。

4. Nonlocal 关键字和 Unboundlocalerror 异常

人们在创建闭包时,有时会遇到UnboundLocalError异常。让我们在下面的代码片段中看到这一点:

>>> def running_total_multiplier_creator(n):...     running_total = 0...     def multiplier(number):...         product = number * n...         running_total += product...         return running_total...     return multiplier... >>> running_doubler = running_total_multiplier_creator(2)>>> running_doubler(5)Traceback (most recent call last):  File '<stdin>', line 1, in <module>  File '<stdin>', line 5, in multiplierUnboundLocalError: local variable 'running_total' referenced before assignment

乍一看,该
running_total_multiplier_creator函数似乎可以有效生成闭包。但是,当我们使用生成的闭包时,UnboundLocalError会引发异常。这个异常是什么意思?如果你阅读 Traceback 消息,你会发现有问题的代码是running_total += product. 为什么会导致这样的错误?这是解释:

  1. 这行代码本质上解释为running_total = running_total + product.

  2. 变量查找顺序称为 LEGB 规则,遵循局部 -> 封闭 -> 全局 -> 内置的顺序。当你running_total = xxx在内部函数中运行时,你是在内部函数的局部范围内注册一个局部变量。当Python继续运行代码到赋值语句右边的时候,又遇到了这个running_total变量,于是Python开始查找这个变量,在局部作用域中找到它。然而,它注意到这个变量还没有被赋值,因为它的赋值语句还没有完成它的执行,这就是为什么错误消息说:local variable 'running_total’ referenced before assignment。当一个变量在赋值之前被引用时,它被称为未绑定错误。

如果我们退一步看看这个multiplier函数,你可能会意识到我们想要使用在函数作用域内running_total定义的变量,从函数的角度来看
running_total_multiplier_creator,它被称为封闭作用域。multiplier为了让内部函数明白它running_total不是一个局部变量,我们必须使用nonlocal关键字来明确。这是正确的版本:

>>> def running_total_multiplier_creator(n):... running_total = 0... def multiplier(number):... nonlocal running_total... product = number * n... running_total += product... return running_total... return multiplier... >>> running_doubler = running_total_multiplier_creator(2)>>> running_doubler(5)10

如你所见,我们只是简单地声明它running_total是一个非局部变量,这会指示 Python 在查找该running_total变量时绕过局部作用域。

5. 为什么要闭包?

到目前为止,我们已经回顾了什么是闭包,但你可能想知道我们为什么要麻烦拥有闭包特性?闭包最常见的应用之一是创建装饰函数。尽管你可能不知道闭包,但我敢打赌你可能听说过装饰器。在 Python 中,装饰器是在不影响装饰函数算法的情况下修改其他函数行为的函数。如果你想更深入地了解闭包,请参阅我关于装饰器的文章。

在这里,我将简要概述与我们刚刚学习的闭包相关的装饰器。以下代码向你展示了一个示例:

def simple_logger(func):    def decorated(*args, **kwargs):        print(f'You're about to call {func}')        result = func(*args, **kwargs)        print(f'You just called {func}')        return result    return decorated@simple_loggerdef hello_world():    print('Hello, World!')

这simple_logger是一个装饰器,使用它涉及一个@符号作为其名称的前缀和装饰函数上方的位置。当你调用该hello_world函数时,将发生以下情况:

>>> hello_world()You're about to call <function hello_world at 0x101ce9790>Hello, World!You just called <function hello_world at 0x101ce9790>

这个特性背后的原因是装饰函数实际上是一个闭包。装饰过程在幕后有以下两个步骤:

# 第一步def hello_world():     print('Hello, World!')# 第二步 hello_world = simple_logger(hello_world)

为了证明这hello_world确实是一个闭包,我们可以像之前一样运行以下检查。

>>> hello_world.__code__.co_freevars('func',)>>> hello_world.__closure__[0].cell_contents<function hello_world at 0x101ce9790>
  • 装饰函数hello_world有一个非局部变量func。

  • 绑定值是一个函数。作为相关点,重要的是要知道函数只是 Python 中的常规对象,因此可以将函数视为其他数据模型变量(例如,列表和字典)。

结论

在本文中,我们回顾了闭包的五个最重要的方面。以下是对这些要点的快速回顾:

  • 内部功能和外部功能之间存在区别。闭包是在外部函数中创建的内部函数。

  • 闭包涉及非局部变量的绑定。

  • 一个函数有它的局部作用域。当你使用在函数内创建的变量时,你使用的是局部变量。如果你使用在函数外部创建的变量,则你使用的是非局部变量。

  • 该nonlocal关键字表示应将变量视为非局部变量。通常,LEGB 规则适用于变量查找。但是,使用nonlocal关键字,Python 将被指示在查找变量时绕过局部作用域。

  • 装饰是创建一个闭包来替换其原始函数声明的过程。每个装饰功能都是引擎盖下的一个关闭。 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多