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都是闭包。它们分别 记住 了外部函数调用时传入的x5和x10即使外部函数outer已经执行完毕它们仍然可以访问这些变量。例如下列这一段代码我们可以从堆栈视角深入理解以下fn_outer(10)执行完之后它的参数num110按道理应该随着函数结束被回收了。但我们后续三次调用fn_inner(1)每次都能拿到num110并和num21相加得到11。这就是闭包的魔力内层函数fn_inner捕获了外层函数fn_outer的变量num1并一直保留着它的值。为了更直观地理解我们可以结合内存模型来看方法区fn_outer和fn_inner的代码定义在这里fn_outer执行时会创建fn_inner函数对象。栈内存执行fn_outer(10)时num110被压入栈中执行到return fn_inner时会把fn_inner的引用返回给主函数。fn_outer执行结束它的栈帧被销毁但fn_inner捕获的num110被保留了下来。后续每次调用fn_inner(1)都会创建新的栈帧使用保留的num110和传入的num21计算得到结果。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关键字导致修改外部变量失败❌ 认为多个闭包实例会共享同一个外部变量每个闭包实例有自己独立的环境