13. 闭包函数与装饰器
1. 函数名的多种用法
1.1 方法一:函数名可以当成变量名赋值
def f1(x, y):
print(x + y)
fn = f1
fn(11, 22) # 33
1.2 方法二:函数名可以作为容器类型的元素
def f1(x, y):
print(x + y)
func_dict = {'Add': f1}
func_dict.get('Add')(33, 66) # 99
1.3 方法三:函数名可以作为参数传递给另一个函数使用
def f1(x, y):
return x + y
def f2(a, b, func):
return func(a, b) + a + b
result = f2(a=1, b=2, func=f1)
print(result) # 6
1.4 方法四:函数名可以作为函数的返回值
def f1(x, y):
return x + y
print(id(f1))
def f2():
return f1
print(id(f2()))
2. 闭包函数
2.1 概念
外层函数内部定义一个里层函数,并且里层函数用到了外层函数的变量
闭:定义在函数内部的函数
包:内层函数使用了外层函数名称空间中的名字
def outer():
a = 666
def inner():
print('里层函数', a) # 里层函数使用了外层函数名称空间中的名字
inner()
outer()
2.2 闭包函数的应用场景
闭包传参方式1:外层函数的变量值固定
def outer():
a = 666
def inner():
print('里层函数', a) # 里层函数使用了外层函数名称空间中的名字
return inner
res = outer()
res() # 里层函数 666 相当于inner()
闭包传参方式2:外层函数的变量值不固定
def outer(name):
# 当有实参传入时,name=实参
def inner():
print('里层函数', name) # 里层函数使用了外层函数名称空间中的名字
return inner
res = outer('leomessi')
res() # 里层函数 leomessi 相当于inner()
3. 装饰器
3.1 基础概念
定义:
在不改变被装饰对象原有的“调用方式”和“内部代码”的情况下,给被装饰对象添加新的功能
装饰器的原则:
对扩展开放,对修改封闭
key word:
名称空间、函数名、闭包函数
装饰器的分类:
无参装饰器、有参装饰器
3.2 推导
需求:计算函数的执行时间
# 预备知识:time模块
import time
print(time.time()) # 时间戳,是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数(不考虑闰秒),用于表示一个时间点。
实现方法1:
没有修改被装饰对象的调用方式,也没修改内部代码,但是可扩展性差
import time
def f1():
start = time.time()
time.sleep(3)
end = time.time()
print(f'该函数运行耗时{end - start}s') # 该函数运行耗时3.0012145042419434s
f1()
def f2():
start = time.time()
time.sleep(3)
end = time.time()
print(f'该函数运行耗时{end - start}s') # 该函数运行耗时3.01076340675354s
f2()
实现方法2:
在很多地方都要计算f1、f2运行时间,就用到了函数
将函数名通过形参传入,封装成函数之后,调用方式变了,不符合装饰器原则
import time
def f1():
time.sleep(3)
print('函数f1运行')
def f2():
time.sleep(3)
print('函数f2运行')
def cal(func):
start = time.time()
func()
end = time.time()
print(f'函数{func}运行耗时{end - start}s')
cal(f1)
# 函数f1运行
# 函数<function f1 at 0x000001F911B23E20>运行耗时3.0101499557495117s
cal(f2)
# 函数f2运行
# 函数<function f2 at 0x000001A3B73F3370>运行耗时3.015449047088623s
实现方法3:
方法2给函数体直接传参无法实现装饰器,于是采用另一种给函数体传参的方式(闭包函数)
闭包函数的方式外层函数的变量值固定,无法将其它函数名传入,可扩展性差
import time
def f1():
time.sleep(3)
print('函数f1运行')
def outer():
func = f1 # 真正的f1被outer局部名称空间存储了
def inner():
start = time.time()
func() # 调用了真正的f1函数
end = time.time()
print(f'函数{func}运行耗时{end - start}s')
return inner
res = outer()
res()
# 函数f1运行
# 函数<function f1 at 0x0000025DCD183E20>运行耗时3.0119853019714355s
这种方式也不是真正的装饰器,需要调用res函数才能调用f1函数
实现方法4:
为了解决方法3外层函数变量值固定的问题,采用向外层函数传参的方式
将函数名通过形参传入,增强可扩展性
import time
def f1():
time.sleep(3)
print('函数f1运行')
def f2():
time.sleep(3)
print('函数f2运行')
def outer(func):
def inner():
start = time.time()
func() # 调用了真正的f1函数
end = time.time()
print(f'函数{func}运行耗时{end - start}s')
return inner
# f1 = outer(f1) # 左侧的f1就是一个普通的变量名
# f1() # 看似调用的是f1,其实调用的是inner
# 函数f1运行
# 函数<function f1 at 0x0000021BEC373E20>运行耗时3.0068602561950684s
f2 = outer(f2) # 左侧的f2就是一个普通的变量名
f2() # 看似调用的是f2,其实调用的是inner
f1 = outer(f1)实现了装饰器,f1()即可调用原f1函数,右边的f1是原函数名,存到outer的局部名称空间了,
左边的f1只是一个普通的变量名
小结:
outer()---返回值是inner的函数地址---将inner的函数名重命名为f1或f2
f1=outer(f1)---f1就是outer内inner的内存地址
f1()---outer内的inner执行---func()---f1真正的函数内存地址---f1()
3.3 练习
# 练习:
"""
先定义一个存款、取款函数,用户登录过才能存款、取款,没有登录让先登录
不能改变原本存款、取款的调用方式
"""
def deposit():
print('存款中...')
return True, '取款完成'
def withdraw():
print('存款中...')
return True, '取款完成'
login_dict = {'username': None, 'status': None}
def outer(func):
def inner():
# 在运行withdraw功能之前进行验证是否登录
if login_dict.get('username') is not None:
res = func() # 接收真正函数的返回值
return res # 条件为真,将真正函数的返回值返回
else:
return False, '请先登录' # 条件为假,让用户先登录
return inner
withdraw = outer(withdraw)
res2 = withdraw()
print(res2)
deposit = outer(deposit)
res3 = deposit()
print(res3)
# 多层验证,先验证用户名,再验证密码
def outer(func):
def inner():
# 在运行withdraw功能之前进行验证是否登录成功
if login_dict.get('username') != 'messi':
return False, '用户名错误'
elif login_dict.get('password') != '001':
return False, '请先登录' # 密码错误
else:
res = func()
return res
return inner
withdraw = outer(withdraw)
res2 = withdraw()
print(res2)
3.4 无参装饰器模板
def f1():
pass
def outer(func):
def inner():
执行真正函数之前,可以做的额外操作
# 其它如登录认证等操作一般放在执行真正函数之前
result = func() # 执行真正的函数
执行真正函数之后,可以做的额外操作
对返回的结果进行处理返回
return result # 返回真正函数的返回值
return inner
f1 = outer(f1)
f1()
3.5 有参装饰器
引入
login_dict = {'name': 'messi', 'pwd': '001'}
def withdraw(name, money):
print(f'用户{name}正在取款{money}元')
def outer(func):
def inner(name, money):
if login_dict.get('name'):
return func(name, money)
else:
return False, '请先登录'
return inner
withdraw = outer(withdraw)
withdraw('messi', 10) # 用户messi正在取款10元
可变长参数,兼容所有函数的参数
login_dict = {'name': 'messi', 'pwd': '001'}
def withdraw(name, money):
print(f'用户{name}正在取款{money}元')
def transfer(name, to_other, money):
print(f'用户{name}给{to_other}转账了{money}元')
def outer(func):
def inner(*args, **kwargs):
if login_dict.get('name'):
return func(*args, **kwargs)
else:
return False, '请先登录'
return inner
withdraw = outer(withdraw)
withdraw('messi', 10) # 用户messi正在取款10元
transfer = outer(transfer)
transfer('ronaldo', 'kylian', 20) # 用户ronaldo给kylian转账了20元
3.6 有参装饰器模板
def f1(*args, **kwargs):
pass
def outer(func): # func用于接收被装饰的对象(函数)
def inner(*args, **kwargs):
print('执行真正函数之前,可以做的额外操作')
res = func(*args, **kwargs) # 执行真正的被装饰函数,res的作用是接收原函数被调用后的返回值
print('执行真正函数之后,可以做的额外操作')
对返回的结果进行处理返回
return res # 返回真正函数的返回值
return inner
f1 = outer(f1)
f1()
4. 语法糖
4.1 原理:
1.使用的时候放在真正函数的上方
2.语法糖会自动将下面紧挨着的函数名传给 @ 后面的函数调用
login_dict = {'name': 'messi', 'pwd': '001'}
def outer(func):
def inner(*args, **kwargs):
if login_dict.get('name'):
return func(*args, **kwargs)
else:
return False, '请先登录'
return inner
@outer # 相当于:withdraw = outer(withdraw)
def withdraw(name, money):
print(f'用户{name}正在取款{money}元')
@outer # 相当于:transfer = outer(transfer)
def transfer(name, to_other, money):
print(f'用户{name}给{to_other}转账了{money}元')
4.2 多层语法糖
4.2.1 多层有参装饰器
login_dict = {'name': 'messi', 'role': 'admin'}
def login(func):
# func是withdraw函数的内存地址
def inner(*args, **kwargs):
print('验证登录程序')
if not login_dict.get('name'):
return '请先登录'
else:
return func(*args, **kwargs)
return inner
def permission(func):
# func是login的inner函数地址
def inner(*args, **kwargs):
print('验证权限程序')
if login_dict.get('role') != 'admin':
return '当前权限不能访问该功能'
return func(*args, **kwargs)
return inner
def withdraw(name, money):
print(f'用户{name}正在取款{money}元')
# 必须是登录状态且角色是管理员才能取款
withdraw = login(withdraw) # =左边是一个普通的变量名,右边是login函数返回值(inner函数内存地址)
withdraw = permission(withdraw) # 等价于withdraw = permission(login的inner函数地址)
# 上述语句在执行时,
# 第一步:permission(login的inner)
# 第二步:在permission的里层函数中func为login的inner函数,因此进入到login函数中校验登录
# 第三步:进入login函数执行inner函数,校验登录,执行func,func为withdraw函数的内存地址
# 第四步:执行真正的withdraw
withdraw('ronaldo', 20)
# 验证权限程序
# 验证登录程序
# 用户ronaldo正在取款20元
# 上述代码按从上到下的顺序编写,先包login,再包permission,但是执行时先验证权限,再验证登录,因此要将登录和权限互换位置
# # 这种情况下就是先验证登录,再验证权限
# withdraw = permission(withdraw)
# withdraw = login(withdraw)
# withdraw('ronaldo', 20)
4.2.2 多层语法糖
多层语法糖的包装顺序是:从下往上包装⬆
执行顺序是:从上往下执行⬇
login_dict = {'name': 'messi', 'role': 'admin'}
def login(func):
# func是withdraw函数的内存地址
def inner(*args, **kwargs):
print('验证登录程序')
if not login_dict.get('name'):
return '请先登录'
else:
return func(*args, **kwargs)
return inner
def permission(func):
# func是login的inner函数地址
def inner(*args, **kwargs):
print('验证权限程序')
if login_dict.get('role') != 'admin':
return '当前权限不能访问该功能'
return func(*args, **kwargs)
return inner
@login # withdraw = login(withdraw) 左边with普通函数名,右边with取自上一步,是permission的inner函数
@permission # withdraw=permission(withdraw) 左边withdraw普通变量名,右边真正函数,运行结果是permission的inner函数
def withdraw(name, money):
print(f'用户{name}正在取款{money}元')
# 上述语法糖含义:先包permission,再包login
# 调用函数时,以@login为参考,这里withdraw等价于login(permission中inner函数地址)
withdraw('messi', 10)
4.3 有参语法糖
有参的有参装饰器
login_dict = {'name': 'messi', 'role': 'admin'}
# 定义一个函数,当传入参数为login时,返回login登录验证函数
# 当传入参数为permission时,返回permission权限验证函数
def choice(status):
if status == 'login':
def login(func):
# func是withdraw函数的内存地址
def inner(*args, **kwargs):
print('验证登录程序')
if not login_dict.get('name'):
return '请先登录'
else:
return func(*args, **kwargs)
return inner
return login
elif status == 'permission':
def permission(func):
# func是login的inner函数地址
def inner(*args, **kwargs):
print('验证权限程序')
if login_dict.get('role') != 'admin':
return '当前权限不能访问该功能'
return func(*args, **kwargs)
return inner
return permission
def withdraw(name, money):
print(f'用户{name}正在取款{money}元')
# 以下仅验证登录
# login_a = choice('login') # 拿到login函数
# withdraw = login_a(withdraw)
# withdraw('messi', 10)
# 验证登录程序
# 用户messi正在取款10元
# 先验证登录再验证权限
login_a = choice('login')
permission_a = choice('permission')
withdraw = permission_a(withdraw)
withdraw = login_a(withdraw)
withdraw('ronaldo', 20)
# 验证登录程序
# 验证权限程序
# 用户ronaldo正在取款20元
上述代码改写为有参语法糖
login_dict = {'name': 'messi', 'role': 'admin'}
# 定义一个函数,当传入参数为login时,返回login登录验证函数
# 当传入参数为permission时,返回permission权限验证函数
def choice(status):
if status == 'login':
def login(func):
# func是withdraw函数的内存地址
def inner(*args, **kwargs):
print('验证登录程序')
if not login_dict.get('name'):
return '请先登录'
else:
return func(*args, **kwargs)
return inner
return login
elif status == 'permission':
def permission(func):
# func是login的inner函数地址
def inner(*args, **kwargs):
print('验证权限程序')
if login_dict.get('role') != 'admin':
return '当前权限不能访问该功能'
return func(*args, **kwargs)
return inner
return permission
@choice('login')
@choice('permission')
def withdraw(name, money):
print(f'用户{name}正在取款{money}元')
withdraw('kylian', 20)
验证登录程序
验证权限程序
用户kylian正在取款20元
4.4 装饰器的修复技术
解决内存地址问题
做到和真的一模一样,但是本质其实没有变
help可以查看指定函数的注释信息
def outer(func):
def inner(*args, **kwargs):
"""这是inner函数的注释"""
result = func(*args, **kwargs)
return result
return inner
@outer
def f1():
"""这是f1函数的注释"""
print('f1函数已运行')
help(f1) 目前的以假乱真还有瑕疵,看到的不是真正函数f1的注释
# Help on function inner in module __main__:
#
# inner(*args, **kwargs)
# 这是inner函数的注释
使用wraps模块,将装饰器中inner函数的内存地址换成了原函数f1的内存地址,实现了和原函数一致的效果
from functools import wraps
def outer(func):
@wraps(func) # 将inner的内存地址换成了原函数f1的内存地址,实现了和原函数一致的效果
def inner(*args, **kwargs):
"""这是inner函数的注释"""
result = func(*args, **kwargs)
return result
return inner
@outer
def f1():
"""这是f1函数的注释"""
print('f1函数已运行')
help(f1)
# Help on function f1 in module __main__:
#
# f1()
# 这是f1函数的注释