Python 闭包与装饰器从入门到精通(一)
目录
前言
第一章 闭包详解:从作用域到函数的 "数据封装"
1.1 前置知识:Python 变量作用域与 LEGB 规则
1.1.1 局部作用域 (Local, L)
1.1.2 嵌套作用域 (Enclosing, E)
1.1.3 全局作用域 (Global, G)
1.1.4 内置作用域 (Built-in, B)
1.1.5 global与nonlocal关键字
1.2 嵌套函数:函数内部定义函数
1.3 闭包的定义与形成条件
1.3.1 什么是闭包?
1.3.2 闭包形成的三个必要条件
1.3.3 最简单的闭包示例
1.4 闭包的执行过程与原理
1.4.1 __closure__属性:查看闭包引用的自由变量
1.5 闭包的优缺点与使用场景
1.5.1 闭包的优势
1.5.2 闭包的潜在问题
1.5.3 闭包的典型应用场景
1.6 闭包核心知识点总结
前言
在 Python 的世界里,闭包和装饰器是两个既优雅又强大的特性。它们不仅是 Python 函数式编程的核心基石,更是众多 Python 框架(如 Flask、Django、FastAPI)的灵魂所在。从路由注册、权限验证到日志记录、性能统计,几乎所有需要 "无侵入式增强函数功能" 的场景,都能看到装饰器的身影。
然而,对于很多初学者来说,闭包和装饰器往往是 Python 学习路上的第一个 "拦路虎"。它们的概念抽象,执行过程看似 "反直觉",尤其是多层嵌套的装饰器和带参数的装饰器,常常让人感到困惑。
本文将从最基础的变量作用域开始,由浅入深地带你彻底理解闭包的本质,然后逐步揭开装饰器的神秘面纱。我们将通过大量可运行的代码示例,详细讲解装饰器的各种写法和使用场景,包括处理不同参数和返回值的装饰器、多个装饰器叠加使用、带参数的装饰器等。最后,我们还会深入探讨深浅拷贝的原理,以及如何解决闭包和装饰器中常见的可变对象陷阱。
读完本文,你将能够:
- 彻底理解闭包的形成条件和执行原理
- 熟练掌握装饰器的各种写法和使用技巧
- 灵活运用装饰器解决实际开发中的问题
- 避开闭包和装饰器中的常见陷阱
- 理解深浅拷贝的区别并正确使用它们
第一章 闭包详解:从作用域到函数的 "数据封装"
1.1 前置知识:Python 变量作用域与 LEGB 规则
要理解闭包,首先必须彻底搞清楚 Python 中的变量作用域。所谓 "作用域",就是变量在程序中的可访问范围。Python 遵循LEGB 规则来查找变量,即按照以下顺序依次查找:
1.1.1 局部作用域 (Local, L)
局部作用域是指在函数内部定义的变量,只能在该函数内部访问。
def func(): # 局部变量,仅在func函数内部可访问 x = 10 print(x) # 输出:10 func() # print(x) # 报错:NameError: name 'x' is not defined1.1.2 嵌套作用域 (Enclosing, E)
嵌套作用域是指在嵌套函数中,外部函数的作用域。内部函数可以访问外部函数定义的变量。
def outer(): # 外部函数变量,在嵌套作用域中可访问 y = 20 def inner(): # 内部函数可以访问外部函数的变量y print(y) # 输出:20 inner() outer()1.1.3 全局作用域 (Global, G)
全局作用域是指在模块级别定义的变量,在整个模块的任何地方都可以访问。
# 全局变量,在整个模块中可访问 z = 30 def func(): print(z) # 输出:30 func() print(z) # 输出:301.1.4 内置作用域 (Built-in, B)
内置作用域是指 Python 解释器内置的变量和函数,如print()、len()、int()等。它们在任何地方都可以直接使用。
# 直接使用内置函数print,无需定义 print("Hello World") # 输出:Hello World1.1.5global与nonlocal关键字
当我们在函数内部想要修改全局变量或外部函数变量时,需要使用相应的关键字声明,否则 Python 会将其视为局部变量。
global关键字:声明要修改的是全局变量
count = 0 def increment(): # 声明count是全局变量 global count count += 1 print(count) increment() # 输出:1 increment() # 输出:2 print(count) # 输出:2nonlocal关键字:声明要修改的是外部函数(嵌套作用域)的变量
def outer(): count = 0 def inner(): # 声明count是外部函数的变量 nonlocal count count += 1 print(count) return inner counter = outer() counter() # 输出:1 counter() # 输出:2重要区别:
global用于修改全局作用域的变量nonlocal用于修改嵌套作用域(外部函数)的变量- 两者都不能用于创建新变量,只能修改已经存在的变量
1.2 嵌套函数:函数内部定义函数
在 Python 中,函数是一等公民(First-class Citizen)。这意味着函数可以:
- 作为参数传递给其他函数
- 作为其他函数的返回值
- 赋值给变量
- 在其他函数内部定义
嵌套函数就是在一个函数内部定义另一个函数。
def outer_function(msg): # 外部函数 print("外部函数被调用") def inner_function(): # 内部函数 print("内部函数被调用") print(f"消息:{msg}") # 调用内部函数 inner_function() outer_function("Hello Python")输出:
外部函数被调用 内部函数被调用 消息:Hello Python嵌套函数的生命周期:
- 当外部函数被调用时,内部函数才会被定义
- 内部函数只能在外部函数内部被调用
- 当外部函数执行完毕后,其局部作用域通常会被销毁
但是,闭包的出现打破了这个生命周期规则。
1.3 闭包的定义与形成条件
1.3.1 什么是闭包?
闭包(Closure)是指引用了外部函数作用域中变量的内部函数,并且这个内部函数被返回并在外部函数之外被调用。
在函数嵌套的前提下,内部函数使用了外部函数的变量这种:使用外部函数变量的内部函数称为闭包。
简单来说,闭包就是一个 "记住了" 它被定义时所在环境的函数。
1.3.2 闭包形成的三个必要条件
- 必须有嵌套函数(函数内部定义函数)
- 内部函数必须引用外部函数作用域中的变量
- 外部函数必须返回内部函数
1.3.3 最简单的闭包示例
def outer(x): # 外部函数 def inner(y): # 内部函数引用了外部函数的变量x return x + y # 外部函数返回内部函数 return inner # 调用外部函数,得到内部函数对象 add5 = outer(5) add10 = outer(10) # 调用内部函数 print(add5(3)) # 输出:8 print(add10(3)) # 输出:13 print(outer(10)(10)) #输出:20在这个例子中,add5和add10都是闭包。它们分别 "记住" 了外部函数调用时传入的x=5和x=10,即使外部函数outer已经执行完毕,它们仍然可以访问这些变量。
例如下列这一段代码,我们可以从堆栈视角深入理解以下:
fn_outer(10)执行完之后,它的参数num1=10按道理应该随着函数结束被回收了。- 但我们后续三次调用
fn_inner(1),每次都能拿到num1=10,并和num2=1相加得到11。
这就是闭包的魔力:内层函数fn_inner捕获了外层函数fn_outer的变量num1,并一直保留着它的值。
为了更直观地理解,我们可以结合内存模型来看:
- 方法区:
fn_outer和fn_inner的代码定义在这里,fn_outer执行时,会创建fn_inner函数对象。 - 栈内存:
- 执行
fn_outer(10)时,num1=10被压入栈中,执行到return fn_inner时,会把fn_inner的引用返回给主函数。 fn_outer执行结束,它的栈帧被销毁,但fn_inner捕获的num1=10被保留了下来。- 后续每次调用
fn_inner(1),都会创建新的栈帧,使用保留的num1=10和传入的num2=1计算,得到结果。
- 执行
1.4 闭包的执行过程与原理
很多初学者会疑惑:当外部函数执行完毕后,它的局部作用域应该被销毁了,为什么闭包还能访问外部函数的变量呢?
答案只有一句话:因为闭包会把它用到的外部变量 “特殊保护” 起来,不让 Python 垃圾回收机制销毁它。
1. 正常函数:执行完 → 作用域销毁 → 变量消失
普通函数执行时:
- 会在栈内存创建局部作用域
- 函数里的变量存在这个作用域里
- 函数执行结束 → 作用域销毁 → 变量被回收
所以正常情况下,外部函数执行完,变量确实没了。
2. 闭包:发现内层函数用到外部变量 → 开启 “保护模式”
当 Python 解释器发现:
有内层函数
内层函数使用了外层函数的变量
外层函数把内层函数返回出去(形成闭包)
解释器就会做一件关键事情:
把被使用的外部变量,从 “栈内存” 移动到 “堆内存”
并且给它打上标记:这个变量被闭包引用了,不能回收!
所以:
外部函数执行完
普通局部作用域销毁
但闭包用到的变量被单独保留
这就是闭包能继续访问的根本原因。
让我们通过一个更详细的例子来理解闭包的执行过程:
def make_counter(): count = 0 print(f"外部函数执行,count初始化为:{count}") def counter(): nonlocal count count += 1 print(f"当前计数:{count}") return count print("外部函数即将返回内部函数") return counter # 第一步:调用外部函数make_counter print("=== 第一次调用make_counter ===") counter1 = make_counter() # 第二步:调用返回的内部函数counter1 print("\n=== 第一次调用counter1 ===") counter1() # 输出:当前计数:1 print("\n=== 第二次调用counter1 ===") counter1() # 输出:当前计数:2 # 第三步:再次调用外部函数,创建新的闭包 print("\n=== 第二次调用make_counter ===") counter2 = make_counter() print("\n=== 第一次调用counter2 ===") counter2() # 输出:当前计数:1 print("\n=== 再次调用counter1 ===") counter1() # 输出:当前计数:3输出:
=== 第一次调用make_counter === 外部函数执行,count初始化为:0 外部函数即将返回内部函数 === 第一次调用counter1 === 当前计数:1 === 第二次调用counter1 === 当前计数:2 === 第二次调用make_counter === 外部函数执行,count初始化为:0 外部函数即将返回内部函数 === 第一次调用counter2 === 当前计数:1 === 再次调用counter1 === 当前计数:3执行过程详解:
- 当调用
make_counter()时,Python 创建一个新的局部作用域,变量count被初始化为 0 - 内部函数
counter被定义,它引用了外部作用域的count变量 make_counter()返回内部函数counter,并将其赋值给counter1- 此时,虽然
make_counter()已经执行完毕,但由于counter1引用了它的局部变量count,所以这个局部作用域不会被垃圾回收,而是被保留下来 - 每次调用
counter1()时,Python 都会找到那个被保留的作用域,修改其中的count变量 - 当再次调用
make_counter()时,会创建一个全新的局部作用域和一个全新的闭包counter2,它与counter1互不影响
1.4.1__closure__属性:查看闭包引用的自由变量
Python 为每个函数对象提供了__closure__属性,用于查看该函数是否是闭包,以及它引用了哪些自由变量。
def outer(x): def inner(y): return x + y return inner add5 = outer(5) # 查看__closure__属性 print(add5.__closure__) # 输出:(<cell at 0x...: int object at 0x...>,) # 查看自由变量的值 print(add5.__closure__[0].cell_contents) # 输出:5- 如果一个函数不是闭包,它的
__closure__属性为None - 如果是闭包,
__closure__是一个元组,每个元素对应一个自由变量的cell对象 - 通过
cell_contents属性可以获取自由变量的值
1.5 闭包的优缺点与使用场景
1.5.1 闭包的优势
- 数据封装与隐藏:闭包可以将变量封装在函数内部,只暴露必要的接口,实现了类似面向对象中的 "私有变量" 效果
- 状态保持:闭包可以在多次调用之间保持状态,而不需要使用全局变量
- 延迟计算:可以将计算推迟到真正需要的时候进行
- 函数工厂:可以根据不同的参数生成不同的函数
1.5.2 闭包的潜在问题
- 内存泄漏风险:由于闭包会保留外部函数的作用域,如果闭包被长期引用,可能会导致内存无法及时释放
- 调试困难:闭包的执行过程相对复杂,变量的作用域不直观,增加了调试难度
- 过度使用会降低代码可读性:如果滥用闭包,会使代码变得晦涩难懂
1.5.3 闭包的典型应用场景
- 计数器:如前面的
make_counter例子 - 缓存:保存函数的计算结果,避免重复计算
- 回调函数:在异步编程中,闭包可以方便地携带上下文信息
- 装饰器:这是闭包最重要的应用,我们将在后面详细讲解
1.6 闭包核心知识点总结
- 闭包是引用了外部函数变量的内部函数,并且被返回在外部调用
- 闭包形成的三个必要条件:嵌套函数、引用外部变量、返回内部函数
- 闭包会 "记住" 它被定义时的环境,即使外部函数已经执行完毕
- 使用
nonlocal关键字在内部函数中修改外部函数的变量 __closure__属性可以查看闭包引用的自由变量- 闭包的核心价值在于数据封装和状态保持
常见误区:
- ❌ 认为只要是嵌套函数就是闭包(必须引用外部变量并被返回)
- ❌ 忘记使用
nonlocal关键字导致修改外部变量失败 - ❌ 认为多个闭包实例会共享同一个外部变量(每个闭包实例有自己独立的环境)
