百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Python闭包|你应该知道的常见用例(上)

bigegpt 2024-12-05 09:43 4 浏览

引言

在 Python 编程语言中,闭包通常指的是一个嵌套函数,即在一个函数内部定义的另一个函数。这个嵌套的函数能够访问并保留其外部函数作用域中的变量。这种结构就构成了一个闭包。

闭包在函数式编程语言中非常普遍。在 Python 中,闭包特别有用,因为它使得你可以创建基于函数的装饰器,这是一种非常强大的功能。

通过本教程[1],你将:

  • 了解闭包的概念以及它们在 Python 中的运作方式
  • 掌握闭包的典型应用场景
  • 探索闭包的替代方法 为了更好地理解本教程,你需要对 Python 的一些基本概念有所了解,比如函数、嵌套函数、装饰器、类和可调用对象。

了解 Python 中的闭包

闭包是一种函数,它能够保持对其词法环境的访问,即使该函数在该环境之外被执行。当外部函数返回内部函数时,你将获得一个具有扩展作用域的函数对象。

换言之,闭包是能够捕获它们所处环境(即封闭作用域)中定义的对象的函数,这使得你可以在函数体内使用这些对象。这个特性让你在需要在连续调用之间保持状态信息时,可以利用闭包。

在那些以函数式编程为主的编程语言中,闭包是一种常见的特性,而 Python 也支持闭包,作为其众多特性之一。

在 Python 中,闭包是你在另一个函数内部定义并返回的函数。这个内部函数能够保留在它定义之前非局部作用域中定义的对象。

为了更深入地理解 Python 中的闭包,你首先需要了解内部函数,因为闭包本质上也是内部函数的一种。

内部函数

在 Python 中,内部函数是指在另一个函数内部定义的函数。这类函数可以访问和修改它们所处外部函数(即非局部作用域)中的变量名。

这里有一个简单的例子:

>>> def outer_func():
...     name = "Pythonista"
...     def inner_func():
...         print(f"Hello, {name}!")
...     inner_func()
...

>>> outer_func()
Hello, Pythonista!

>>> greeter = outer_func()
>>> print(greeter)
None

在这个示例中,你在全局范围内定义了 outer_func() 函数。在这个函数内部,你声明了一个名为 local 的局部变量。接着,你定义了另一个名为 inner_func() 的函数。由于这个函数定义在 outer_func() 函数体内,因此它是一个嵌套函数。最终,你调用了这个嵌套函数,它使用了在外部函数中定义的 name 变量。

当你执行 outer_func() 函数时,inner_func() 函数将 name 变量的值嵌入到问候语字符串中,并将其显示在你的屏幕上。

在上述示例中,你定义了一个能够访问封闭作用域中变量的嵌套函数。但是,当你调用外部函数时,你并没有获得对嵌套函数的引用。嵌套函数和局部变量在外部函数之外是不可见的。

在接下来的部分,你将学习如何将嵌套函数转变为闭包,这样你就可以访问嵌套函数和它保留的变量了。

函数闭包

并非所有的嵌套函数都是闭包,但所有的闭包都是嵌套函数。要将一个嵌套函数转变为闭包,你需要从外部函数返回这个嵌套函数的对象。这听起来可能有些拗口,但以下是如何让 outer_func() 函数返回一个闭包对象的方法:

>>> def outer_func():
...     name = "Pythonista"
...     def inner_func():
...         print(f"Hello, {name}!")
...     return inner_func
...

>>> outer_func()
<function outer_func.<locals>.inner_func at 0x1066d16c0>

>>> greeter = outer_func()

>>> greeter()
Hello, Pythonista!

在 outer_func() 函数的这个新版本中,你返回的是 inner_func 函数对象本身,而不是执行它。这样,当你执行 outer_func() 函数时,你将得到一个闭包对象,而不是一条问候信息。这个闭包对象能够记住 name 变量的值,即使 outer_func() 函数执行完毕后,你依然可以通过闭包访问这个值。这就是为什么当你调用 greeter() 函数时能够看到问候信息的原因。

  • 创建一个 Python 闭包需要以下元素:
  1. 一个外部或封闭函数:这个函数包含了另一个函数,通常称为内部函数。外部函数可以接收参数,并定义一些内部函数可以访问和修改的变量。
  2. 仅在外部函数中有效的局部变量:这些变量定义在外部函数的作用域内。Python 会保留这些变量,使得即使在外部函数执行完毕后,你仍然可以在闭包中使用它们。
  3. 一个内部或嵌套函数:这个函数定义在外部函数内部,它能够访问并修改外部函数中的变量,哪怕外部函数已经执行完毕。

在这个部分的例子中,你有一个外部函数、一个局部变量(name)和一个内部函数。要得到一个闭包对象,最后一步是从外部函数返回内部函数对象。

另外值得一提的是,你也可以使用 lambda 表达式来创建闭包:

>>> def outer_func():
...     name = "Pythonista"
...     return lambda: print(f"Hello, {name}!")
...

>>> greeter = outer_func()
>>> greeter()
Hello, Pythonista!

在 outer_func() 函数的这个修改版中,你通过使用 lambda 函数来创建闭包,其功能与原始版本相同。

捕获的变量

如你所了解的,闭包会保留其封闭作用域内的变量。以下是一个简单的示例:

>>> def outer_func(outer_arg):
...     local_var = "Outer local variable"
...     def closure():
...         print(outer_arg)
...         print(local_var)
...         print(another_local_var)
...     another_local_var = "Another outer local variable"
...     return closure
...

>>> closure = outer_func("Outer argument")

>>> closure()
Outer argument
Outer local variable
Another outer local variable

在这个示例中,当你执行 outer_func() 函数时,outer_arg、local_var 和 another_local_var 这些变量都会被绑定到闭包上,哪怕它们原来所在的环境已经不存在了。不过,closure() 函数能够访问这些变量,因为它们现在成为了闭包的一部分。这就是为什么我们说闭包是一个拥有扩展作用域的函数。

闭包同样可以修改这些变量的值,这会导致两种不同的情况:变量可能指向一个不可变对象或者一个可变对象。

如果你想修改指向不可变对象的变量的值,你需要使用 nonlocal 声明。以下是一个示例:

>>> def make_counter():
...     count = 0
...     def counter():
...         nonlocal count
...         count += 1
...         return count
...     return counter
...

>>> counter = make_counter()

>>> counter()
1
>>> counter()
2
>>> counter()
3

在这个示例中,count 变量保存了一个整数值的引用,而整数值是不可变的。如果你想更新 count 的值,就需要使用 nonlocal 声明,这告诉 Python 你希望在非局部作用域中重用这个变量。

当你的变量指向一个可变对象时,你可以直接修改这个变量的值:

>>> def make_appender():
...     items = []
...     def appender(new_item):
...         items.append(new_item)
...         return items
...     return appender
...

>>> appender = make_appender()

>>> appender("First item")
['First item']
>>> appender("Second item")
['First item', 'Second item']
>>> appender("Third item")
['First item', 'Second item', 'Third item']

在这个示例中,变量 items 指向一个列表对象,列表是可变的。因此,你无需使用 nonlocal 关键字,可以直接在原地修改这个列表。

利用闭包保持状态

在实际应用中,你可以在多种不同场合使用 Python 闭包。在本节中,你将了解如何利用闭包来创建工厂函数,跨函数调用保持状态,以及实现回调机制,这将使你的代码更加动态、灵活和高效。

创建工厂函数

你可以编写一些函数来生成带有初始配置或参数的闭包。这在你想要创建多个具有不同设置的相似函数时非常有用。

例如,假设你需要计算不同度数和结果精度的数值根。在这种情况下,你可以编写一个工厂函数,该函数返回预设了度数和精度的闭包,如下所示:

>>> def make_root_calculator(root_degree, precision=2):
...     def root_calculator(number):
...         return round(pow(number, 1 / root_degree), precision)
...     return root_calculator
...

>>> square_root = make_root_calculator(2, 4)
>>> square_root(42)
6.4807

>>> cubic_root = make_root_calculator(3)
>>> cubic_root(42)
3.48

make_root_calculator() 是一个工厂函数,用于生成计算不同数值根的函数。在这个函数中,你传入根的度数和期望的精度作为配置参数。

接着,你定义了一个内部函数,该函数接受一个数字作为输入,并计算出具有所需精度的特定根。最终,你返回这个内部函数,从而创建了一个闭包。

你可以利用这个函数来创建闭包,以便计算不同度数的数值根,例如平方根和立方根。需要注意的是,你还可以调整计算结果的精度。

构建有状态的函数

闭包可以用来在函数调用之间保持状态,这些函数被称为有状态函数,而闭包是实现它们的一种方法。

例如,假设你想编写一个函数,它从数据流中连续获取数值,并计算它们的累积平均值。在函数的多次调用之间,必须保留之前传递的值。在这种情况下,你可以使用如下所示的函数:

>>> def cumulative_average():
...     data = []
...     def average(value):
...         data.append(value)
...         return sum(data) / len(data)
...     return average
...

>>> stream_average = cumulative_average()

>>> stream_average(12)
12.0
>>> stream_average(13)
12.5
>>> stream_average(11)
12.0
>>> stream_average(10)
11.5

在 cumulative_average() 函数中,data 这个局部变量使得你能够在连续调用这个函数返回的闭包对象时保持状态。

然后,你创建了一个名为 stream_average() 的闭包,并用不同的数值来调用它。注意这个闭包是如何记住之前传入的值,并通过累加新提供的价值来计算平均值的。

提供回调函数

闭包在事件驱动编程中非常常见,当你需要创建携带额外上下文或状态信息的回调函数时就会用到。图形用户界面(GUI)编程是这类回调函数的一个典型应用场景。

例如,假设你想使用 Tkinter(Python 的标准 GUI 编程库)来创建一个显示 "Hello, World!" 的应用程序。这个应用程序需要一个标签来展示问候语,以及一个按钮来触发显示。以下是这个小程序的代码:

import tkinter as tk

app = tk.Tk()
app.title("GUI App")
app.geometry("320x240")

label = tk.Label(
    app,
    font=("Helvetica", 16, "bold"),
)
label.pack()

def callback(text):
    def closure():
        label.config(text=text)

    return closure

button = tk.Button(
    app,
    text="Greet",
    command=callback("Hello, World!"),
)
button.pack()

app.mainloop()

这段代码创建了一个简单的 Tkinter 应用,包含一个窗口,窗口里有一个标签和一个按钮。点击 "Greet" 按钮后,标签会展示 "Hello, World!" 的信息。

callback() 函数会返回一个闭包对象,这个对象可以用来设置按钮的命令参数。这个参数需要一个不接受任何参数的可调用对象。如果你需要像示例那样传递参数,那么可以使用闭包。

[1]Source: https://realpython.com/python-closure/

相关推荐

悠悠万事,吃饭为大(悠悠万事吃饭为大,什么意思)

新媒体编辑:杜岷赵蕾初审:程秀娟审核:汤小俊审签:周星...

高铁扒门事件升级版!婚宴上‘冲喜’老人团:我们抢的是社会资源

凌晨两点改方案时,突然收到婚庆团队发来的视频——胶东某酒店宴会厅,三个穿大红棉袄的中年妇女跟敢死队似的往前冲,眼瞅着就要扑到新娘的高额钻石项链上。要不是门口小伙及时阻拦,这婚礼造型团队熬了三个月的方案...

微服务架构实战:商家管理后台与sso设计,SSO客户端设计

SSO客户端设计下面通过模块merchant-security对SSO客户端安全认证部分的实现进行封装,以便各个接入SSO的客户端应用进行引用。安全认证的项目管理配置SSO客户端安全认证的项目管理使...

还在为 Spring Boot 配置类加载机制困惑?一文为你彻底解惑

在当今微服务架构盛行、项目复杂度不断攀升的开发环境下,SpringBoot作为Java后端开发的主流框架,无疑是我们手中的得力武器。然而,当我们在享受其自动配置带来的便捷时,是否曾被配置类加载...

Seata源码—6.Seata AT模式的数据源代理二

大纲1.Seata的Resource资源接口源码2.Seata数据源连接池代理的实现源码3.Client向Server发起注册RM的源码4.Client向Server注册RM时的交互源码5.数据源连接...

30分钟了解K8S(30分钟了解微积分)

微服务演进方向o面向分布式设计(Distribution):容器、微服务、API驱动的开发;o面向配置设计(Configuration):一个镜像,多个环境配置;o面向韧性设计(Resista...

SpringBoot条件化配置(@Conditional)全面解析与实战指南

一、条件化配置基础概念1.1什么是条件化配置条件化配置是Spring框架提供的一种基于特定条件来决定是否注册Bean或加载配置的机制。在SpringBoot中,这一机制通过@Conditional...

一招解决所有依赖冲突(克服依赖)

背景介绍最近遇到了这样一个问题,我们有一个jar包common-tool,作为基础工具包,被各个项目在引用。突然某一天发现日志很多报错。一看是NoSuchMethodError,意思是Dis...

你读过Mybatis的源码?说说它用到了几种设计模式

学习设计模式时,很多人都有类似的困扰——明明概念背得滚瓜烂熟,一到写代码就完全想不起来怎么用。就像学了一堆游泳技巧,却从没下过水实践,很难真正掌握。其实理解一个知识点,就像看立体模型,单角度观察总...

golang对接阿里云私有Bucket上传图片、授权访问图片

1、为什么要设置私有bucket公共读写:互联网上任何用户都可以对该Bucket内的文件进行访问,并且向该Bucket写入数据。这有可能造成您数据的外泄以及费用激增,若被人恶意写入违法信息还可...

spring中的资源的加载(spring加载原理)

最近在网上看到有人问@ContextConfiguration("classpath:/bean.xml")中除了classpath这种还有其他的写法么,看他的意思是想从本地文件...

Android资源使用(android资源文件)

Android资源管理机制在Android的开发中,需要使用到各式各样的资源,这些资源往往是一些静态资源,比如位图,颜色,布局定义,用户界面使用到的字符串,动画等。这些资源统统放在项目的res/独立子...

如何深度理解mybatis?(如何深度理解康乐服务质量管理的5个维度)

深度自定义mybatis回顾mybatis的操作的核心步骤编写核心类SqlSessionFacotryBuild进行解析配置文件深度分析解析SqlSessionFacotryBuild干的核心工作编写...

@Autowired与@Resource原理知识点详解

springIOCAOP的不多做赘述了,说下IOC:SpringIOC解决的是对象管理和对象依赖的问题,IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系...

java的redis连接工具篇(java redis client)

在Java里,有不少用于连接Redis的工具,下面为你介绍一些主流的工具及其特点:JedisJedis是Redis官方推荐的Java连接工具,它提供了全面的Redis命令支持,且...