Python——第四章:闭包(Closure)、装饰器(Decorators)
闭包:
本质, 内层函数对外层函数的局部变量的使用. 此时内层函数被称为闭包函数
1. 可以让一个变量常驻与内存,可随时被外层函数调用。
2. 可以避免全局变量被修改、被污染、更安全。(通过版本控制工具,将不同人所写的代码都整合的时候,避免出现问题)
def func():
a = 10
def inner():
print(a)
return a
return inner
ret = func()
代码定义了一个函数 func
,它返回了另一个函数 inner
。这种结构被称为闭包(closure),因为 inner
函数引用了在其外部定义的变量 a
。在这里,a
是 func
函数的局部变量,但由于 inner
函数引用了它,a
的值在 inner
函数被调用时仍然是可用的。
这段代码可以实现的特殊效果:
1、因为迟迟没有使用ret()
调用ret
(inner
函数),程序为了能够持续提供可用性,会将该段代码常驻于内存。不会被内存回收。
2、用函数来定义局部变量a,并且局部变量不会被后续函数外的代码操控、改变,仅可以被赋值后读取、打印。不能被其他全局变量修改。实现局部变量仅可以在本函数内部才可以被操作。a被保护起来了。
3、想调用这个局部变量a,可以随时调用ret()
函数,使得局部变量既可以被调用,还不会被修改。
未来某一时刻,ret()
调用了 func
函数并将其结果赋值给变量 ret
。此时,ret
包含了 inner
函数。如果你调用 ret()
,它将输出 10
,因为 inner
函数引用了外部的 a
变量,而 a
的值在 func
函数被调用时被设置为 10
。
再看下面一段代码
def func():
a = 0
def inner():
nonlocal a
a += 1
return a
return inner
ret = func()
a = 20 #此时即使出现了全局变量a=20,也不会干扰到func函数局部变量a的计数器累计
# inner => ret => 什么时候执行
r1 = ret()
print(r1) #第一次输出,结果为1
# 可能1000000行后才执行
r2 = ret()
print(r2) #第二次输出,结果为2
print(ret()) #结果为3
print(ret()) #结果为4
这段代码,常驻于内存,有实现内部计数器的作用。
装饰器
装饰器本质上是一个闭包
作用:在不改变原有函数调用的情况下. 给函数增加新的功能.(直白地说: 可以在函数前后添加新功能, 但是不改原来的代码)
哪里会用到装饰器:
- 程序用户登录的地方
- 操作日志(增、删、改、查)
- 可以在函数前后添加新功能, 但是不改变原来的代码功能。
一、推导装饰器需要用到的原理:
- 函数可以做为参数进行传递(代理执行)
def func(): print('我是函数') def gggg(fn): # fn要求是个函数 fn() # func() gggg(func)
- 函数可以作为返回值进行返回(闭包)
def func(): def inner(): print("123") return inner ret = func() ret()
- 函数名称可以当成变量一样进行赋值操作
def func1(): print("我是函数1") def func2(): print("我是函数2") func1 = func2 func1()
二、装饰器的推导过程:
1.定义2个游戏,运行2个游戏
def play_dnf():
print("开始玩dnf游戏")
def play_lol():
print("开始玩lol游戏")
play_dnf()
play_lol()
2.此时我们出现了新的需求:
- 在运行dnf游戏前,先打开外挂;在结束游戏后,关闭外挂。
- 在运行lol游戏前,先打开外挂;在结束游戏后,关闭外挂。
- 我们要用游戏管家自动搞这2个事情
因此我们又试图定义了一个管家,并且用代理执行的逻辑(函数可以做为参数进行传递)传输函数执行.
def guanjia(game):
print("打开外挂")
game() # 玩起来了
print('关闭外挂')
def play_dnf():
print("开始玩dnf游戏")
def play_lol():
print("开始玩lol游戏")
guanjia(paly_dnf)
guanjia(paly_lol)
但是我们运行后发现:打游戏的主题不是我,而是管家 。这样就非常的不合理。因为运行的主体变了。我们只想要管家负责在我玩游戏之前开外挂、我玩游戏之后关外挂,而不是代理打游戏。
因此我们使用了:
1、闭包的玩法(函数可以作为返回值进行返回);
2、函数名称可以当成变量一样进行赋值操作,再次对游戏进行封装;
def guanjia(game):
def inner():
print("打开外挂")
game() # 玩起来了
print('关闭外挂')
return inner
def play_dnf():
print("开始玩dnf游戏")
def play_lol():
print("开始玩lol游戏")
play_dnf = guanjia(play_dnf) # 让管家把游戏重新封装一遍. 我这边把原来的游戏替换了
play_lol = guanjia(play_lol) # 让管家把lol也重新封装一下.
play_dnf() # 此时运行的是管家给的内层函数inner
play_lol()
解读:这里是把play_dnf
(实参)传给game
(形参),然后用return
返回inner
,把打开外挂、玩游戏、关闭外挂一切都返回给(全新的)打包的play_dnf = guanjia(play_dnf)
,让一切都是我执行的。
这样操作就把原先的管家打游戏,变回了我打游戏,只是这一切都是“我自己操作的”。主体没有发生变化。
*注意:因为这种写法play_dnf = guanjia(play_dnf)
非常不容易阅读,容易造成混淆,并且有更好的替代写法——在运行程序的前面进行标记(@guanjia
),每次你要运行程序之前,guanjia就会自动加载封装的代码。
def guanjia(game):
def inner():
print("打开外挂")
game() # 玩起来了
print('关闭外挂')
return inner
@guanjia
def play_dnf():
print("开始玩dnf游戏")
@guanjia # 相当于 play_dnf = guanjia(play_lol)
def play_lol():
print("开始玩lol游戏")
play_dnf() # 因为前面有@guanjia,这里就相当于 play_dnf = guanjia(play_dnf)
play_lol() # 因为前面有@guanjia,这里就相当于 play_dnf = guanjia(play_lol)
至此,我们搞到了装饰器的雏形:
def wrapper(fn): #wrapper: 装饰器, fn: 目标函数
def inner:
pass # 在目标函数执行之前操作
fn() # 执行目标函数
pass # 在目标函数执行之后操作
return inner #千万别加()
@wrapper #让装饰器代理运行目标函数
def target(): #目标函数
pass
target() # 实际上这里运行的是wrapper里的 =>inner()
被装饰函数的参数问题
新的问题又来了:比如目标函数中有参数,打lol游戏或者dnf要输入账号和密码
def play_dnf(username, password):
print("打开dnf,输入账号密码。 ", username, password)
print('开始玩dnf游戏!')
play_dnf(admin,123456)
#运行结果
打开dnf,输入账号密码。 admin 123456
开始玩dnf游戏!
不加装饰器,我们这样是可以正常执行程序的。
但是加这个装饰器雏形里是没有办法加入任何参数的
def guanjia(game):
def inner():
print("打开外挂")
game() # 玩起来了
print('关闭外挂')
return inner
@guanjia
def play_dnf(username, password):
print("打开dnf,输入账号密码。 ", username, password)
print('开始玩dnf游戏!')
play_dnf("admin","123456")
#执行结果
play_dnf("admin", "123456")
TypeError: guanjia.<locals>.inner() takes 0 positional arguments but 2 were given #在guanjia的局部变量的inner里,没有位置接收变量,但是程序却给了2个
Process finished with exit code 1
TypeError: guanjia.<locals>.inner() takes 0 positional arguments but 2 were given
在guanjia的局部变量的inner里,没有位置接收变量,但是程序却给了2个
因此我们需要改造装饰器的def inner()
为def inner(username,password)
如下执行
def guanjia(game):
def inner(username, password):
print("打开外挂")
game() # 玩起来了
print('关闭外挂')
return inner
@guanjia
def play_dnf(username, password):
print("打开dnf,输入账号密码。 ", username, password)
print('开始玩dnf游戏!')
play_dnf("admin", "123456")
#执行结果
打开外挂
Traceback (most recent call last):
File "D:\装饰器.py", line 13, in <module>
play_dnf("admin", "123456")
File "D:\装饰器.py", line 4, in inner
game() # 玩起来了
^^^^^^
TypeError: play_dnf() missing 2 required positional arguments: 'username' and 'password'
Process finished with exit code 1
第13行存在的参数,给到第4行的game()
时,缺少了2个参数'username' and 'password'
为此我们也需要将game()
变成game(username, password)
,把参数也给到game中,并将来一起作为inner
返回给原函数做调用(来回穿透传输),程序就顺利执行起来了.
def guanjia(game):
def inner(username, password):
print("打开外挂")
game(username, password) # 玩起来了
print('关闭外挂')
return inner
@guanjia
def play_dnf(username, password):
print("打开dnf,输入账号密码。 ", username, password)
print('开始玩dnf游戏!')
play_dnf("admin", "123456")
#执行结果
打开外挂
打开dnf,输入账号密码。 admin 123456
开始玩dnf游戏!
关闭外挂
但是,新的问题又来了,dnf暂时解决了,但是再加入lol的时候,又来了新问题,lol的参数更多,还有hero.
def guanjia(game):
def inner(username, password):
print("打开外挂")
game(username, password) # 玩起来了
print('关闭外挂')
return inner
@guanjia
def play_dnf(username, password):
print("打开dnf,输入账号密码。", username, password)
print("开始玩dnf游戏!")
@guanjia
def play_lol(username, password, hero):
print("打开lol,输入账号密码,选择英雄。", username, password, hero)
print("开始玩lol游戏!")
play_dnf("admin", "123456")
play_lol("admin", "456789", "盖伦")
#执行结果
play_lol("admin", "456789", "盖伦")
TypeError: guanjia.<locals>.inner() takes 2 positional arguments but 3 were given
TypeError: guanjia.<locals>.inner() takes 2 positional arguments but 3 were given
在guanjia的局部变量的inner里,有2个位置函数接收变量,但是程序却给出了3个。
这里为了要让guanjia接收到任意函数,并回传给目标函数。我们得采用通用写法,可实现通用调用。
def guanjia(game):
# *和**表示接收所有参数, *把所有位置参数打包成元组;**把所有关键字参数打包成字典。
def inner(*args, **kwargs): # 给inner添加了参数, args一定是一个元组;kwargs一定是字典(admin, 123456, "大盖伦")
print("打开外挂")
# *, ** 表示把args元组打散成位置参数,以及把kwargs字典打散成关键字参数,传递进game()去
game(*args, **kwargs) # 玩起来了 # game('admin', '123456', "大盖伦")
print('关闭外挂')
return inner
@guanjia
def play_dnf(username, password):
print("打开dnf,输入账号密码。", username, password)
print("开始玩dnf游戏!")
@guanjia
def play_lol(username, password, hero):
print("打开lol,输入账号密码,选择英雄。", username, password, hero)
print("开始玩lol游戏!")
play_dnf("admin", "123456")
play_lol("admin", "456789", "盖伦")
注意:在game(*args, **kwargs)
这个表达式中,*args
的作用是将元组中的元素解包,分别作为位置参数传递给func
函数。这就是所谓的“打散”操作。同样地,**kwargs
会将字典中的键值对解包成关键字参数传递给game
。
可以参考阅读:实参位置调用列表和字典——最后的两个案例。
被装饰函数的返回值问题:
说完了参数问题,还要考虑返回值问题,比如玩dnf掉落:屠戮之刃:return"掉落:屠戮之刃"
,在没有guanjia处理的时候应该这样操作函数,接收返回值:
def play_dnf(username, password):
print("打开dnf,输入账号密码。", username, password)
print("开始玩dnf游戏!")
return"掉落:屠戮之刃"
ret=play_dnf("admin", "123465")
print(ret)
这里我们可以分析到,return是需要在guanjia
的game()
中运行后获得的返回值,为此我们应该给game()
做一个ret
接收,并且在inner
里return ret
def guanjia(game):
def inner(*args, **kwargs):
print("打开外挂")
game(*args, **kwargs) # =>应该在这里接收返回值
print('关闭外挂')
return inner
因此,整个代码应该
def guanjia(game):
def inner(*args, **kwargs):
print("打开外挂")
ret = game(*args, **kwargs) # 这里是目标函数的执行, 这里是能够从目标函数拿到返回值的.
print('关闭外挂')
return ret #在这里将返回值返回全局(局部变量)
return inner
@guanjia
def play_dnf(username, password):
print("打开dnf,输入账号密码。", username, password)
print("开始玩dnf游戏!")
return"掉落:屠戮之刃"
ret = play_dnf("admin", "123465") #这里虽然也叫ret,但是是全局变量,不要混淆
print(ret)
至此,整个装饰器的推导过程完成了。
通用装饰器的写法:
def wrapper(fn): wrapper: 装饰器, fn: 目标函数
def inner(*args, **kwargs):
pass # 在目标函数执行之前.....
ret = fn(*args, **kwargs) # 执行目标函数
pass # 在目标函数执行之后.....
return ret
return inner # 千万别加()
@wrapper
def target():
pass
ret = target() # 在外层,执行内层函数 =>inner()
一个函数可以被多个装饰器装饰:
@wrapper1
@wrapper2
def target():
print('我是目标')
规则和规律:wrapper1进入——wrapper2进入——target()——wrapper2出去——wrapper1出去
def wrapper1(fn): # fn: wrapper2.inner
def inner(*args, **kwargs): # 1
print("这里是wrapper1 进入") # 2
ret = fn(*args, **kwargs) # 3 => wrapper2.inner
print("这里是wrapper1 出去") # 11
return ret # 12
return inner # 13
def wrapper2(fn): # fn: target
def inner(*args, **kwargs): # 4
print("这里是wrapper2 进入") # 5
ret = fn(*args, **kwargs) # 6 => taget
print("这里是wrapper2 出去") # 8
return ret # 9
return inner # 10
@wrapper1 # target = wrapper1(wrapper2.inner) => target: wrapp1.inner
@wrapper2 # target = wrapper2(target) => target: wrapper2.inner
def target():
print('目标函数') # 7
target() # 0 => 从这里开始
#运行结果
这里是wrapper1 进入 #套壳顺序4 #输出顺序1
这里是wrapper2 进入 #套壳顺序2 #输出顺序2
目标函数 #套壳顺序1 #输出顺序3
这里是wrapper2 出去 #套壳顺序3 #输出顺序4
这里是wrapper1 出去 #套壳顺序5 #输出顺序5
这里可以看到,目标函数在结构上的规律是:
- 最中间是目标函数
- 先被离着最近的
@wrapper2
上下套壳 - 再被上层的
@wrapper1
上下套壳 - 套壳顺序和输出顺序是不一样的
*大家自行掌握这个规律即可,不用关心我做的序号。这里只是为了方便展示,我自己做了个顺序标记。
装饰器也可以传参
装饰器的实战操作
登录状态判断:login_flag
账号密码判断:while 1
、break
login_flag = False
def login_verify(fn):
def inner(*args, **kwargs):
global login_flag
if login_flag == False: # 关键判断位置*****
# 这里完成登录校验
print('还未完成用户登录操作')
while 1: # 无脑循环登录
username = input("输入你的账号")
password = input("输入你的密码")
if username == "admin" and password == "123":
print("登录成功")
login_flag = True
break #成功后退出循环
else:
print("登录失败, 用户名或密码错误")
ret = fn(*args, **kwargs) # 后续程序的执行
return ret
return inner
@login_verify
def add():
print("添加员工信息")
@login_verify
def delete():
print("删除信息")
@login_verify
def upd():
print("修改信息")
@login_verify
def search():
print("查询员工信息")
add()
upd()
delete()
search()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)