函数之闭包函数、装饰器
函数之闭包函数、装饰器
在说明闭包函数和装饰器之前,我们先补充一下,函数名要怎么用。
函数名的多种用法
函数名做变量名
-
函数名有等同变量名的功能
def func(): print('from func') print(func) # <function func at 0x000002558C1471F0> ''' 后面是存放这个函数代码的内层地址 其实函数名就可以看做变量名,只是它绑定的数据地址是一段函数体代码 ''' res = func # 将变量的绑定传递给另一个变量 res() # res也和函数名一样,可以加括号调用函数功能了
所以本质上,函数名也是变量名的一种。
-
函数名可以做实参传给函数
既然函数名是变量名,那么函数自然可以像变量一样作为参数传入函数了
def func(): print('from func') def index(func_name): print('go to index') func_name() print('from index') index(func) """运行结果 go to index from func from index """
从运行结果可以清晰的看到,func的代码是在index函数体代码执行的,
说明真的将函数名通过参数的方式传入了函数中 -
函数名可以做返回值被输出
def func1(): def func2(): print('from func2') return func2 func_name = func1() # func1运行结束返回了func2的函数名 func_name() # from func2
在这里函数名被作为返回值返回出来,仔细分析,我们实际上做到了:
在全局中调用了func1局部名称空间中的函数名func2
这一点,在后续的闭包函数和装饰器中有很好的应用,这里先埋个伏笔。
函数名存放于容器类型之中(如字典)
结合函数名是本质是变量名的特点,我们可以对分支结构做很好的优化。
当一个py文件有很多的分支,每个分支的功能相对独立,可以被封装为一个个小函数,我们之前是这么操作的:
def func1():
print('func1')
def func2():
print('func2')
def func3():
print('func3')
def func4():
print('func4')
# 之前的分支结构,用if-elif来分支
print('1:注册|2:登录|3:删除|4:查看')
choice = input('输入功能对应数字:')
if choice == '1':
func1()
elif choice == '2':
func2()
elif choice == '3':
func3()
elif choice == '4':
func4()
else:
print('没有相关功能')
很显然,这样的分支结构不仅写起来繁琐,而且扩展起来也十分的麻烦,每扩展一次,elif就要再写一次,加入新的功能。
所以我们将功能函数作为变量存储到字典中,就可以大幅减少分支结构带给我们的麻烦:
func_dict = {
'1': func1,
'2': func2,
'3': func3,
'4': func4,
} # 代号与函数的对应关系
print('1:注册|2:登录|3:删除|4:查看')
choice = input('输入功能对应数字:')
# 分支结构变成了搜索字典
if choice in func_dict:
func_dict.get(choice)() # 从字典中取到函数名套上括号就可以使用了
else:
print('没有相应的功能代号')
闭包函数
闭包函数是指:
- 定义在函数体内部的函数
- 内部的函数用到了外部函数的变量
- 满足上述描述的函数,内层函数叫闭函数,外层函数叫包函数
def outer(): # 外层函数
x = 100
def inner(): # 内层函数
print(x) # 内层函数用到了外层函数的变量
闭包函数的意义在于它是第二种对函数进行传参的方式,且这种传参方式有其独特的优点。
# 原本的传参方式
def func(wifi_name, wifi_pwd):
print(f'wifi名称是:{wifi_name}|wifi密码是{wifi_pwd}')
func('leethon的WiFi', 123)
func('leethon的WiFi', 123)
func('leethon的WiFi', 123)
func('lalisa的WiFi', 321)
# 每次调用,我们都需要往函数传实参。
# 闭包函数传参方式
def func(wifi_name, wifi_pwd):
def inner():
print(f'wifi名称是:{wifi_name}|wifi密码是{wifi_pwd}')
return inner
lee_wifi = func('leethon的WiFi', 123)
lee_wifi()
lee_wifi()
lee_wifi()
lisa_wifi = func('lalisa的WiFi', 321)
lisa_wifi()
lee
# 只有第一次调用,我们需要传参数,在参数不变的情况下,无需再次传参
像当我们再调用函数时拥有一套常见的配置作为参数传入,用闭包函数的方法就会很方便。
装饰器理论推导
装饰器能在调用者不改变调用方式和不改动原函数体代码的情况下,给函数添加功能,他满足:
- 不改变原函数的调用方式
- 不改动原函数的函数体代码
- 新功能独立于它装饰的函数,新功能可以装饰多个函数
以下面的需求为例,我们来盘一盘怎么才能得到一个装饰器
import time # 导入时间模块,用这个模块可以用一些时间相关的功能
def index():
print('index in')
time.sleep(2) # 程序等待2秒
print('index out')
index()
# index原本的功能是in 等两秒 然后out
给这个函数增加功能,统计这个函数的运行时间。
装饰器雏形
-
在函数体代码外直接添加代码
start = time.time() # 时间戳,是一个绝对的时间概念 index() end = time.time() print(f'程序运行的时间是{end - start}') # 结束时间 - 开始时间得到了函数运行时间
这样我们就在函数不变的情况下,添加了功能,但是添加的功能必须写在调用内容的前后,每次调用并不方便,我们应该需要函数把这个添加的功能保存下来。
-
外层嵌套函数封装原功能和新功能
def outer(): start = time.time() index() end = time.time() print(f'程序运行的时间是{end - start}') outer() # 只要调用outer就能新旧功能一起用了
这个新添加的功能绑定死给了index,其他的函数要想用这个新功能就得再写一个新函数嵌套。
-
传入参数更改函数
def outer(func): start = time.time() func() end = time.time() print(f'程序运行的时间是{end - start}') outer(index) # 运行了这一句调用新功能和旧功能
我们虽然添加了新的功能,也实现了一定的兼容性,但是改变了调用方式,依旧不满足装饰器的定义。
-
第二种传参方式传入函数名
def outer(func): def inner(): start = time.time() func() end = time.time() print(f'程序运行的时间是{end - start}') return inner res = outer(index) # 通过闭包函数拿到了inner, res() # inner运行既新添了功能,内部被装饰的函数也可以被替换 '''但是调用方式还是产生了一定的变化,这时破局点就在赋值语句''' index = outer(index) # 将这个添加了新功能的新函数名(被修饰好的函数)赋值给原函数名 '''我们先在等号右边得到了inner函数名,再赋给左边''' index() # 符合1.不改变原函数|2.不改变调用方式|3.装饰了新的功能
至此,我们已经得到了修饰器的雏形了,但是仍然存在一些兼容性的缺陷,我们需要继续讨论。
装饰器完全体
上一小节,我们提到装饰器雏形有些兼容性的缺陷,是什么呢,该如何解决
-
装饰器雏形只能装饰无参函数
# 如果想要以下有参函数也被装饰,用原本的装饰器就不行了 # 装饰器雏形 ''' def outer(func): def inner(): start = time.time() func() end = time.time() print(f'程序运行的时间是{end - start}') return inner ''' # 想被装饰的有参函数 def waste_time(n): time.sleep(n) # 提前导入过了time模块,n是几就等待几秒 # 没有被装饰的函数运行 waste_time(2) # 等了两秒 # 装饰语句 waste_time = outer(waste_time) # 再尝试调用装饰好的有参函数 waste_time(1) # 报错:没有要求参数,却给了1个参数
也就是说,我们所写的装饰器雏形,只能装饰无参函数。
想装饰有一个参数的函数就得写成下面的形式。
def outer(func): def inner(a): # 这里能接收一个位置函数 start = time.time() func(a) # 这里再原封不动还给要装饰的有参函数 end = time.time() print(f'程序运行的时间是{end - start}') return inner
同样的,这样写,这个新的装饰器也只能装饰恰好有一个参数的函数罢了。
那我们的装饰器,对被装饰的函数的参数个数要求太高了。
为了解决这一问题,我们要用到可变长参数。
def outer(func): def inner(*args, **kwargs): # 这里能接收所有的位置参数和关键字参数 start = time.time() func(*args, **kwargs) # 这里再原封不动还给要装饰的有参函数 end = time.time() print(f'程序运行的时间是{end - start}') return inner # 试一下效果 def waste_time(n): print(n) time.sleep(2) def func(a, b): print(a, b) time.sleep(1) waste_time(1) # 执行了原有功能 func(2, 3) # 执行了原有功能 waste_time = outer(waste_time) func = outer(func) waste_time(1) # 在打印了1的基础上,统计了时间约等于2秒 func(2, 3) # 在打印了2,3的基础上,统计了时间约等于1秒
使用上述装饰器已经解决了参数个数不定的问题,无论有多少参数,以位置实参还是关键字实参的形式传,都可以按照原本函数的传参方式去传,不会影响调用方式。
-
装饰器雏形不能装饰有返回值的函数
如果被装饰的函数有返回值,在装饰后会不会发生变化?
答案是会发生变化,如下
def outer(func): def inner(*args, **kwargs): # 这里能接收所有的位置参数和关键字参数 start = time.time() func(*args, **kwargs) # 这里再原封不动还给要装饰的有参函数 end = time.time() print(f'程序运行的时间是{end - start}') return inner def func(a,b,c): print(a,b,c) time.sleep(3) # 等待三秒 return a+b+c print(func(1,2,3)) # 返回6 func = outer(func) # 装饰语句 print(func(1,2,3)) # 返回None
被装饰后的函数返回了none,和原本不一样,
也就是说虽然调动的方式没变,也执行了原本的功能,也添加了新的功能,但是返回值变了。
原因就是,我们本质上是用装饰器中的inner替换了原函数,但是inner是没有返回值的,所以返回了none。所以应该将inner添加return使它有一个返回值,且等于原函数的返回值。
def outer(func): def inner(*args, **kwargs): # 这里能接收所有的位置参数和关键字参数 start = time.time() res = func(*args, **kwargs) # 接收原函数的返回值 end = time.time() print(f'程序运行的时间是{end - start}') return res # 给inner一个返回值,就是原函数的返回值 return inner # 试一下 def func(a,b,c): print(a,b,c) time.sleep(3) # 等待三秒 return a+b+c print(func(1,2,3)) # 返回6 func = outer(func) # 装饰语句 print(func(1,2,3)) # 返回6
装饰器模板
至此,我们就可以在不改动原函数,不改动调用方式,不改动传参方式,不改动返回值的情况下,给原函数添加新的功能了。
上面的一切理论最终得到的结论就是一个模板:
def outer(func):
def inner(*args, **kwargs):
# print('新添加功能代码前')
res = func(*args, **kwargs)
# print('新添加功能代码后')
return res
return inner
装饰器语法糖
每次转换的语句func = outer(func)
比较麻烦,而且实际写代码时由于位置不固定也十分的乱。
python提供了一个简单语法,来代替这一条语句:
@outer # 紧贴着要被装饰的函数前,代表func = outer(func)
def func():
"""被装饰函数"""
pass
多层语法糖
有时候,我们在加装一个装饰器后,还会继续加装饰器。
那时,我们就会在函数上面贴上好几个装饰器,如以下:
@outer1 # 装饰器1
@outer2 # 装饰器2
@outer3 # 装饰器3
def index():
print('from index')
形成了一种多层语法糖的结构,在加装了多个语法糖时,实际的执行顺序是先加装最贴近被装饰函数的装饰器,如上述例子中,会先用outer3装饰index,然后用outer2再装饰,最后outer1再装饰。
拓展:多层语法糖的运行过程
来分析以下的例子,我们向index加装了三个装饰器,这些代码的7条打印语句会按什么顺序执行呢?
def outer1(func1):
print('加载了outter1')
def wrapper1(*args, **kwargs):
print('执行了wrapper1')
res1 = func1(*args, **kwargs)
return res1
return wrapper1
def outer2(func2):
print('加载了outter2')
def wrapper2(*args, **kwargs):
print('执行了wrapper2')
res2 = func2(*args, **kwargs)
return res2
return wrapper2
def outer3(func3):
print('加载了outter3')
def wrapper3(*args, **kwargs):
print('执行了wrapper3')
res3 = func3(*args, **kwargs)
return res3
return wrapper3
@outer1 # index = outer1(加载了outer3的index即wrapper2)>>>wrapper1——>index
@outer2 # outer2(加载了outer3的index即wrapper3)>>>wrapper2
@outer3 # outer3(原本的index)>>>wrapper3
def index():
print('from index')
index()
上述代码执行顺序:
- 定义了三个装饰器,只检查装饰器的语法,不执行函数体代码
- 定义了index,按照outer3、outer2、outer1的顺序装饰index
- index作为实参传入outer3,开始运行outer3代码,打印加载outer3,
- 定义wrapper3并返回了wrapper3
- wrapper3被outer2装饰,作为实参传入outer2,开始运行outer2代码,打印加载outer2,
- 定义wrapper2并返回了wrapper2
- wrapper2被outer1装饰,作为实参传入outer1,开始运行outer1,打印加载outer1,
- 定义并返回了wrapper1,并将wrapper1赋值给index,index()相当于调用wrapper1()
- 执行index(),相当于调用wrapper1()
- 开始执行wrapper1,先打印,再执行func1(参数)
- func1形参被传入的是wrapper2,相当于调用wrapper2
- 开始执行wrapper2,先打印,再执行func2
- func2形参被传入的是wrapper3,
- 开始执行wrapper3,先打印‘执行wrapper3’,再执行func3
- func3形参被传入了index,开始执行index,打印‘from index’
- index执行结束,返回wrapper3,wrapper3也随之执行结束,逐级返回,结束了每级的调用。
有参装饰器
上文所用到的装饰器,是无参装饰器,而还有一种装饰器叫有参装饰器。
无参装饰器中,新加装的功能代码不能从外界接收参数。
# 无参装饰器模板
def outer(func):
def inner(*args, **kwargs):
print(a, b) # 现在我们想在这个装饰器中传入参数
res = func(*args, **kwargs)
return res
return inner
问题来啦,a和b变量从哪里获取?
-
位置1:在inner的形参传入
不行,inner是最终替换被装饰函数的,这样会改变被装饰函数的调用方式。
-
位置2:在outer的函数体内定义a,b
这样的变量定义后就被写死了,不行
-
位置3:在outer的形参传入a和b
理论上可以通过
index = outer(index, a, b)
来执行,但是这样就没法使用语法糖了,因为@outer
只能传入紧贴下方的函数名做形参。
所以我们就要在外层再套一层函数来闭包式传参了:
def outer_plus(a, b):
def outer(func):
def inner(*args, **kwargs):
print(a, b) # 现在我们想在这个装饰器中传入参数
res = func(*args, **kwargs)
return res
return inner
return outer
# 这样我们就可以往装饰器里面传入参数了
# 并且可以使用语法糖升级版
@outer_plus(1, 2)
def func(bla, blabla):
print('from func')
这个有参装饰器的语法糖的运行规则如下:
-
函数名(参数)的组合优先执行
outer_plus(1, 2)传入实参,在outer_plus的函数体中定义了outer函数,并作为返回值返回
-
outer作为返回值返回后,组成了@outer语法糖
也就是将紧贴下方的func传入outer,定义了inner并返回了inner,其中所有的参数都可以从各级名称空间拿到。
最终用inner替换了原本的func。
无参与有参装饰器模板
# 无参装饰器
def outer(func):
def inner(*args, **kwargs):
# 新追加功能
res = func(*args, **kwargs)
return res
return inner
# 有参装饰器
def outer(装饰器自己的参数)
def inner(func):
def wrapper(*args, **kwargs):
# 新追加功能
res = func(*args, **kwargs)
return res
return wrapper
return inner
装饰器修复技术
语法介绍:help内置函数
def index():
"""
我是index,我有很强大的功能
"""
pass
help(index)
'''运行结果
Help on function index in module __main__:
index()
我是index,我有很强大的功能
'''
help会帮助我们拿到函数的名字以及注释信息,但是在我们使用装饰器时,因为被装饰的函数被替换成wrapper函数,导致我们的注释信息被覆盖了。
def outer(func):
def wrapper(*args, **kwargs):
'''我是wrapper,我喜欢换'''
res = func(*args, **kwargs)
return res
return wrapper
@outer
def index():
"""我是index,我有很强大的功能"""
pass
help(index)
'''运行结果
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
我是wrapper,我喜欢换
'''
可以看到注释和函数名都被替换了,为了防止装饰器对原函数的注释信息进行覆盖,我们可以利用别人写好的一个功能来对此进行修复:
from functools import wraps # 导入别人的功能
def outer(func):
@wraps(func) # 本质是个有参装饰器
def wrapper(*args, **kwargs):
'''我是wrapper,我喜欢换'''
res = func(*args, **kwargs)
return res
return wrapper
@outer
def index():
"""我是index,我有很强大的功能"""
pass
help(index)
"""运行结果
Help on function index in module __main__:
index()
我是index,我有很强大的功能
"""
# 相当于使我们的装饰器替换的函数更像原函数了
练习
练习1
对以下四个功能添加登录认证的装饰器,在使用这些功能的时候,必须要经过登录认证,认证成功后能使用所有功能,并不再需要再次认证。register|login|transfer|withdraw
user = None
def login_auth(func):
def inner(*args, **kwargs):
global user
if not user:
username = input('用户名:').strip()
password = input("密码:").strip()
if username == 'leethon' and password == '123':
user = 'leethon'
print('登录成功')
else:
print('用户名或者密码错误,请重新输入')
return
res = func(*args, **kwargs)
return res
return inner
@login_auth
def register():
print('--注册功能--')
@login_auth
def login():
print('--登录功能--')
@login_auth
def transfer():
print('--转账功能--')
@login_auth
def withdraw():
print('--提款功能--')
func_dict = {
'1': register,
'2': login,
'3': transfer,
'4': withdraw,
'0': quit
}
while True:
print('0:退出|1:注册|2:登录|3:转账|4:提款')
choice = input('请输入你的功能代码:')
if choice in func_dict:
func_dict.get(choice)()
else:
print('请输入正确的功能代码')
练习2
使用有参装饰器编写多种用户登录校验策略
# 用户字典存临时用户
user_dict = {}
# 在文件存了年费会员的信息userinfo.txt
auth_dict = dict.fromkeys(['admin_user', 'temp_user', 'vip_user'],None)
# 每次启动程序将认证三种认证设为没有认证
# 装饰器编写
def login_auth(mode):
def inner(func):
def wrapper(*args, **kwargs):
# 管理员登录后可以进行注册功能和管理功能
if mode == '管理员登录':
if not auth_dict.get('admin_user'):
print('此功能需要管理员权限,请先登录')
username = input('管理员的用户名:').strip()
password = input("管理员的密码:").strip()
if username == 'leethon' and password == '123':
auth_dict['admin_user'] = 'leethon'
print('管理员登录成功')
else:
print('用户名或者密码错误,请重新输入')
return
res = func(*args, **kwargs)
return res
# 需要临时卡片才能进入会所
elif mode == '用户字典校验':
# 其他功能临时会员或年费会员都可以使用
if not auth_dict.get('temp_user') or not auth_dict.get('vip_user'):
# 临时会员程序结束后就消失
if not user_dict:
print('还没有注册临时用户')
return
print('此功能需要临时卡,请先登录')
username = input('临时用户名:').strip()
password = input("临时密码:").strip()
if username in user_dict and password == user_dict.get(username):
auth_dict['temp_user'] = username
print('临时会员登录成功')
else:
print('用户不存在或密码不正确')
return
res = func(*args, **kwargs)
return res
# 需要年费会员才能开通特殊服务
elif mode == '本地文件校验':
if not auth_dict.get('vip_user'):
# 打开文件校验密码
print('此功能需要vip权限,请先登录')
username = input('vip账户名:').strip()
password = input("至尊密码:").strip()
with open(rf'userinfo.txt', 'r', encoding='utf8')as f:
for line in f:
name, pwd = line.strip().split('|')
if name == username and pwd == password:
auth_dict['vip_user'] = username
print('年费用户登录成功')
break
else:
print('用户不存在或密码不正确')
return
res = func(*args, **kwargs)
return res
return wrapper
return inner
@login_auth('管理员登录')
def register():
order = input('输入序号,选择注册【1临时|2年费】会员:')
username = input('你要注册的用户名:').strip()
password = input("输入注册的密码:").strip()
if order == '1':
user_dict[username] = password
print(f'临时会员{username}注册成功')
elif order == '2':
with open(rf'userinfo.txt', 'a', encoding='utf8')as f:
f.write(f'{username}|{password}\n')
print(f'年费会员{username}注册成功')
else:
print('输入了错误的设置')
@login_auth('管理员登录')
def admin():
print('--管理功能--')
@login_auth('用户字典校验')
def let_in():
print('--进入会所--')
@login_auth('本地文件校验')
def serve():
print('--捏脚捶背--')
func_dict = {
'1': register,
'2': admin,
'3': let_in,
'4': serve,
'0': quit
}
while True:
print('0:退出|1:注册|2:登录|3:进店|4:特殊服务')
choice = input('请输入你的功能代码:')
if choice in func_dict:
func_dict.get(choice)()
else:
print('请输入正确的功能代码')
作业3
"""
1.先编写校验用户身份的装饰器
2.然后再考虑如何保存用户登录状态
3.再完善各种需求
"""
user_data = {
'1': {'name': 'jason', 'pwd': '123', 'access': ['1', '2', '3']},
'2': {'name': 'kevin', 'pwd': '321', 'access': ['1', '2']},
'3': {'name': 'oscar', 'pwd': '222', 'access': ['1']}
}
hashmap = {v.get('name'): k for k, v in user_data.items()}
print(hashmap)
login_user_index = None
def login_auth(mode):
def inner(func):
def wrapper(*args, **kwargs):
global login_user_index
# 如果登录过了,就直接校验权限,并执行功能
if not login_user_index:
# 选择想用的功能时要求登录
username = input('用户名:').strip()
password = input('密码:').strip()
# 校验密码
if username in hashmap:
user_index = hashmap.get(username)
user_dict = user_data.get(user_index)
if user_dict.get('pwd') == password:
# 保存登录状态
login_user_index = hashmap.get(username)
print('登录成功')
else:
print('账户或者密码错误')
return
# 核验用户权限
if login_user_index:
if mode in user_data.get(login_user_index).get('access'):
res = func(*args, **kwargs)
return res
print('抱歉,你没有此功能的权限')
return wrapper
return inner
@login_auth('1')
def func1():
"""func1"""
print('func1')
@login_auth('2')
def func2():
"""func2"""
print('func2')
@login_auth('3')
def func3():
"""func3"""
print('func3')
func_dict = {
'0': quit,
'1': func1,
'2': func2,
'3': func3,
}
while True:
choice = input('功能序号:').strip()
if choice in func_dict:
func_dict.get(choice)()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人