深入理解Python闭包与装饰器:从入门到进阶

张开发
2026/4/18 1:10:48 15 分钟阅读

分享文章

深入理解Python闭包与装饰器:从入门到进阶
目录一、全局变量和局部变量1. 作用域2. 生命周期3. 全局变量与局部变量的访问范围4. 问题为什么在全局作用域中无法访问局部变量呢二、闭包1. 闭包的概念和作用2.闭包的格式3. 闭包需要满足三个条件4. 注意事项5. 在闭包的内部实现对外部变量的修改6. 闭包的综合案例三、装饰器入门1. 什么是装饰器2. 格式3. 装饰器雏形传统方式4. 装饰器定义(语法糖方式)5. 装饰器案例获取程序的执行时间四、装饰器进阶1. 带有参数装饰器2. 带有返回值装饰器3. 通用版本的装饰器4. 装饰器高级使用装饰器传递参数五、补充内容1. 多个装饰器的执行顺序2. 使用 functools.wraps 保留原函数信息3. 类装饰器4. 装饰器的典型应用场景六、总结在Python编程中闭包和装饰器是两个紧密相连且非常强大的特性。闭包是实现装饰器的基础而装饰器则是对闭包的经典应用。在学习它们之前我们先再重温一下全局变量和局部变量。一、全局变量和局部变量1. 作用域全局变量是函数内外都能访问局部变量是只能在函数内访问。2. 生命周期全局变量伴随着当前主程序的调用而创建伴随着主程序的结束而销毁。局部变量伴随着当前所在函数的调用而创建伴随着函数的结束而销毁。3. 全局变量与局部变量的访问范围① 在全局作用域中可以访问全局变量在局部作用域中可以访问局部变量# 全局作用域全局变量 num1 10 def func(): # 局部作用域局部变量 num2 20 # 在局部访问局部变量 print(num2) #20 # 在全局访问全局变量 print(num1) #10 # 调用函数 func()② 在局部作用域中可以访问全局变量# 全局作用域全局变量 num1 10 def func(): # 局部作用域局部变量 # 在局部作用域中可以访问全局变量 print(num1) #10 # 调用函数 func()③ 在全局作用域中不能访问局部变量# 全局作用域全局变量 num1 10 def func(): # 局部作用域局部变量 num2 20 # 调用函数 func() # 在全局作用域中调用局部变量num2 print(num2) # 报错4. 问题为什么在全局作用域中无法访问局部变量呢答主要原因在于在Python的底层存在一个“垃圾回收机制”主要的作用就是回收内存空间加快计算机的运行。我们在Python代码中定义的变量也是需要占用内存的所以Python为了回收已经被使用过的内存会自动将函数运行以后的内部变量和程序直接回收。当调用完函数后函数内定义的变量就销毁了,那么如何让局部变量再多待一会,不立刻销毁呢即可不可以改变函数内变量的生命周期呢答案是肯定的———可以通过闭包来实现二、闭包1. 闭包的概念和作用​概念在函数嵌套的前提下内部函数使用了外部函数的变量并且外部函数返回了内部函数我们把这个使用外部函数变量的内部函数称为闭包。作用闭包可以保存函数内的变量而不会随着调用完函数而被销毁。2.闭包的格式def 外部函数名(局部变量): def 内部函数名(): # 内部函数使用了外部函数的局部变量 return 内部函数地址3. 闭包需要满足三个条件有嵌套外部函数内嵌套了内部函数有引用内部函数用了外部函数中的局部变量有返回外部函数返回了内部函数名实际上就是返回内部函数的地址 闭包程序三步走1、有嵌套 2、有引用 3、有返回 def func(): num 20 # 局部变量 def inner(): print(num) return inner # 实际上inner函数并没有执行只是返回了inner函数在内存中的地址 f func() # 相当于把inner在内存中的地址0x...赋值给变量f f() # 找到inner函数的内存地址并执行器内部的代码num20)在于闭包函数保留了num20这个局部变量 # 输出结果 20闭包的作用正常情况下当执行func()的时候函数内部的变量num 20会随着函数的func函数的结束而被垃圾回收机制所回收。所以闭包的真正作用就是可以在全局作用域中实现间接对局部变量进行访问。4. 注意事项由于闭包引用了外部函数的变量所以外部函数的变量并没有及时释放消耗内存。5. 在闭包的内部实现对外部变量的修改错误版本 Python闭包① 有嵌套 ② 有引用 ③ 有返回 def outer(): num 10 def inner(): # 这种写法无法实现通过闭包修改外部的局部变量 num 20 print(outer函数中的num, num) # 10 inner() # 执行函数inner让num20生效 print(outer函数中的num, num) # 10 return inner f outer() f() # 运行结果 outer函数中的num 10 outer函数中的num 10正确版本nonlocal关键字在函数内部修改函数外部的变量这个变量非全局变量global关键字在函数内部声明变量代表引用全局作用域中的全局变量 Python闭包① 有嵌套 ② 有引用 ③ 有返回 def outer(): num 10 def inner(): # 这种写法无法实现通过闭包修改外部的局部变量 nonlocal num num 20 print(outer函数中的num, num) # 10 inner() # 执行函数inner让num20生效 print(outer函数中的num, num) # 20 return inner f outer() f() # 运行结果 outer函数中的num 10 outer函数中的num 20global和nonlocal核心区别global定义全局变量在任意函数内修改全局变量。onlocal在有嵌套函数的前提下只能在内部函数中修改外部函数的局部变量。6. 闭包的综合案例闭包的作用可以在全局作用域中间接访问局部变量在函数执行以后def func(): result 0 def inner(num): nonlocal result result num print(result) return inner f func() f(1) # 1 f(2) # 3分析执行f func()的时候result赋值为0然后定义inner返回inner最终结果f inner函数的内存地址执行f(1)相当于执行inner函数nonlocal引用局部变量result0然后进行1操作弹出011继续执行执行f(2)相当于执行inner函数声明nonlocal result代表还是引用外部的局部变量由于此时外部的result已经被f(1)更改为1了所以由于局部变量一直没有消失所以此时result1执行2操作最终结果为3注意闭包会延长外部变量的生命周期如果滥用可能导致内存占用增加。三、装饰器入门1. 什么是装饰器装饰器在不改变现有函数源代码以及函数调用方式的前提下实现给函数增加额外的功能使用装饰器中的内部函数充当原有函数使用。装饰器的本质就是一个闭包函数。2. 格式def 外部函数名(局部变量): def 内部函数名(): # TODO 在不改变原始函数基础上,添加额外功能 内部函数使用了外部函数的局部变量 return 内部函数地址3. 装饰器雏形传统方式语法变量名 装饰器名(原有函数名)变量名(假设我们有一个评论功能需要先登录才能执行。我们可以用闭包包装一下# 要求把登录功能封装起来比如封装成一个函数添加这个登录不能影响现有功能函数 装饰器本质是一个闭包有嵌套、有引用、有返回返回的是函数的内存地址 参数fn在check中也是一个局部变量 参数fn就是要装饰的函数的函数名如comment如download def check(fn): def inner(): # 开发登录功能 print(登录功能) # 调用原函数 fn() return inner # 评论功能前提登录 def comment(): print(评论功能) comment check(comment) comment() # 下载功能前提登录 def download(): print(下载功能) download check(download) download() # 运行结果 登录功能 评论功能 登录功能 下载功能上面的 comment check(comment) 可以简化为 check 放在函数定义上方。4. 装饰器定义(语法糖方式)语法装饰器名def check(fn): def inner(): # 开发登录验证功能 print(验证登录) # 执行原有函数 fn() return inner check def comment(): print(发表评论) comment() # 运行结果 验证登录 发表评论5. 装饰器案例获取程序的执行时间# 定义获取程序的执行时间装饰器 import time def get_time(fn): def inner(): # ① 添加装饰器修饰功能获取程序的执行时间 begin time.time() # ② 调用fn函数执行原函数代码 fn() end time.time() print(f这个函数的执行时间{end - begin}) return inner get_time def demo(): sum0 for i in range(1000000): sumi print(sum) demo() # 运行结果 499999500000 这个函数的执行时间0.05813407897949219四、装饰器进阶1. 带有参数装饰器 带有参数的装饰器① 有嵌套 ② 有引用 ③ 有返回 def logging(fn): def inner(*args, **kwargs): # 添加装饰器代码输出日志信息 print(-- 日志信息... --) # 执行要修饰的函数 fn(*args, **kwargs) # sum_num(a, b) return inner logging def sum_num(*args, **kwargs): result 0 # *args代表不定长元组参数args (10, 20) for i in args: result i # **kwargs代表不定长字典参数 kwargs {a:30, b:40} for i in kwargs.values(): result i print(result) # sum_num带4个参数而且类型不同10和20以元组形式传递a30b40以字典形式传递 sum_num(10, 20, a30, b40) # 运行结果 -- 日志信息... -- 1002. 带有返回值装饰器 带有返回值的装饰器① 有嵌套 ② 有引用 ③ 有返回 如果一个函数执行完毕后没有return返回值则默认返回None def logging(fn): def inner(*args, **kwargs): print(-- 日志信息... --) return fn(*args, **kwargs) # fn() sub_num(20, 10) result return inner logging def sub_num(a, b): result a - b return result print(sub_num(20, 10)) # 运行结果 -- 日志信息... -- 103. 通用版本的装饰器 通用装饰器① 有嵌套 ② 有引用 ③ 有返回 ④ 有不定长参数 ⑤ 有return返回值 def logging(fn): def inner(*args, **kwargs): # 输出装饰器功能 print(-- 正在努力计算 --) # 调用fn函数 return fn(*args, **kwargs) return inner logging def sum_num1(a, b): result a b return result print(sum_num1(20, 10)) logging def sum_num2(a, b, c): result a b c return result print(sum_num2(10, 20, 30)) # 运行结果 -- 正在努力计算 -- 30 -- 正在努力计算 -- 604. 装饰器高级使用装饰器传递参数注意装饰器一次只能接收一个参数基本语法def 装饰器(fn): ... 装饰器(参数) def 函数(): # 函数代码实例代码根据传递参数不同打印不同的日志信息 通用装饰器① 有嵌套 ② 有引用 ③ 有返回 ④ 有不定长参数 ⑤ 有return返回值 真正问题通过装饰器传递参数我们应该如何接收这个参数呢 答在logging方法的外侧在添加一个函数专门用于接收传递过来的参数 def logging(flag): # flag 或 flag - def decorator(fn): def inner(*args, **kwargs): if flag : print(-- 日志信息正在进行加法运算 --) elif flag -: print(-- 日志信息正在进行减法运算 --) return fn(*args, **kwargs) return inner return decorator logging() def sum_num(a, b): result a b return result logging(-) def sub_num(a, b): result a - b return result print(sum_num(10, 20)) print(sub_num(100, 80)) # 运行结果 -- 日志信息正在进行加法运算 -- 30 -- 日志信息正在进行减法运算 -- 20五、补充内容1. 多个装饰器的执行顺序多个装饰器叠加时靠近函数的装饰器先执行由内向外包装由外向内执行。python def deco1(fn): def inner(): print(deco1 开始) fn() print(deco1 结束) return inner def deco2(fn): def inner(): print(deco2 开始) fn() print(deco2 结束) return inner deco1 deco2 def hello(): print(Hello) hello() # 运行结果 deco1 开始 deco2 开始 Hello deco2 结束 deco1 结束2. 使用 functools.wraps 保留原函数信息装饰器会覆盖原函数的 __name__、__doc__ 等属性。使用 wraps 可以解决这个问题。python from functools import wraps def my_decorator(fn): wraps(fn) def inner(*args, **kwargs): return fn(*args, **kwargs) return inner my_decorator def say_hello(): 这是一个打招呼的函数 print(Hello) print(say_hello.__name__) # 输出 say_hello而不是 inner print(say_hello.__doc__) # 输出文档字符串3. 类装饰器除了函数类也可以用作装饰器。需要实现 __call__ 方法。python class CountCalls: def __init__(self, fn): self.fn fn self.count 0 def __call__(self, *args, **kwargs): self.count 1 print(f调用次数{self.count}) return self.fn(*args, **kwargs) CountCalls def test(): print(执行函数) test() test() # 运行结果 调用次数1 执行函数 调用次数2 执行函数4. 装饰器的典型应用场景日志记录自动记录函数调用信息权限校验检查用户是否登录或具有权限性能计时计算函数执行时间缓存缓存函数返回值事务处理数据库操作的自动提交/回滚输入验证检查参数合法性六、总结1. 闭包是函数嵌套、引用外部变量、返回内部函数的组合体它可以保留外部函数的局部变量实现在全局作用域中间接访问局部变量。2. 装饰器是闭包最经典的应用它可以在不修改原函数代码和调用方式的前提下动态地添加额外功能。3. 从简单的无参装饰器到处理参数和返回值的通用装饰器再到接收参数的装饰器工厂理解装饰器的关键在于理解函数也是对象以及闭包的作用域规则。4. 使用 functools.wraps 可以避免装饰器覆盖原函数的元数据是一个良好的编程习惯。

更多文章