Python中的闭包和装饰器
这里写目录标题
- 一. 为什么在全局作用域中无法访问局部变量
- 二. 我们有没有办法把函数内部的局部变量保留
- 三. 闭包
- 1. 闭包的构成条件(三步走)
- ① 有嵌套
- ② 有引用
- ③ 有返回(return)
- 2. 在闭包的内部实现对外部变量的修改
- 3. 闭包的综合案例
- 四. 装饰器
- 1. 什么是装饰器
- 2. 装饰器的雏形
- 3. 装饰器定义
- 4. 装饰器的作用:获取程序的执行时间
- 5. 带有参数装饰器
- 6. 带有返回装饰器
- 7. 通用版本的装饰器(以后所有的装饰器以此为准)
- 8. 装饰器高级:使用装饰器传递参数
- 8. 扩展:类装饰器
一. 为什么在全局作用域中无法访问局部变量
因为计算机中的垃圾回收机制
# 全局作用域(全局变量)
a = 10
def func():
# 局部作用域(局部变量)
b = 20
print(a)
print(a)
# 调用函数
func()
# 在全局作用域中调用局部变量b,出错
print(b)
#在全局作用域中可以访问全局变量,在局部作用域中可以访问局部变量
#在局部作用域中可以访问全局变量, 在全局作用域中不能访问局部变量
a,全局变量,在整个程序中有效,引用计数器为1
b,局部变量,在func函数执行时有效,引用计数器为1
func执行完毕,b的引用计数器为0
系统循环回收垃圾时,把引用计数器为0的变量回收
此为垃圾回收机制
二. 我们有没有办法把函数内部的局部变量保留
答:使用闭包
在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数称为闭包。
f=outer()相当于把inner在内存中的地址赋值给变量f
f()找到inner函数的内存地址,inner函数引用了b,b的引用计数器为1,在闭包函数inner里保留了b=20这个局部变量
三. 闭包
1. 闭包的构成条件(三步走)
① 有嵌套
② 有引用
③ 有返回(return)
'''
闭包程序三步走:① 有嵌套 ② 有引用 ③ 有返回
'''
def func():
num = 20 # 局部变量
def inner():
print(num)
return inner # 实际上inner函数并没有执行,只是返回了inner函数在内存中的地址
f = func() # 相当于把inner在内存中的地址0x7fbc9b3f8e18赋值给变量f
f() # 找到inner函数的内存地址,并执行器内部的代码(num=20),在于闭包函数保留了num=20这个局部变量
闭包的作用:正常情况下,当执行func()的时候,函数内部的变量num = 20,会随着函数的func函数的结束而被垃圾回收机制所回收。所以闭包的真正作用:就是可以在全局作用域中,实现间接对局部变量进行访问。
闭包的注意事项:由于闭包引用了外部函数的变量,则外部函数的变量没有及时释放,消耗内存。
2. 在闭包的内部实现对外部变量的修改
新知识点:nonlocal关键字(在函数内部修改函数外部的变量,这个变量非全局变量)
老知识点:global关键字(在函数内部声明变量,代表引用全局作用域中的全局变量)
'''
Python闭包:① 有嵌套 ② 有引用 ③ 有返回
'''
def outer():
num = 10
def inner():
# 这种写法无法实现通过闭包修改外部的局部变量'
nonlocal num
num = 20
print('outer函数中的num:', num) # 10
inner() # 执行函数inner,让num=20生效
print('outer函数中的num:', num) # 20
return inner
f = outer()
f()
f=outer()执行红色框代码
f()执行蓝色框代码
3. 闭包的综合案例
闭包的作用:可以在全局作用域中间接访问局部变量(在函数执行以后)
'''
闭包编写三步走:① 有嵌套 ② 有引用 ③ 有返回
分析:
执行f = func()的时候,result赋值为0,然后定义inner,返回inner,最终结果f = inner函数的内存地址
执行f(1),相当于执行inner函数,nonlocal引用局部变量result=0,然后进行+1操作,弹出0+1=1
继续执行
执行f(2),相当于执行inner函数,声明nonlocal result,代表还是引用外部的局部变量,由于此时外部的result已经被
f(1)更改为1了,所以由于局部变量一直没有消失,所以此时result=1,执行+2操作,最终结果为3
'''
def func():
result = 0
def inner(num):
nonlocal result
result += num
print(result)
return inner
f = func()
f(1) # 1
f(2) # 3
四. 装饰器
1. 什么是装饰器
在不改变现有函数源代码以及函数调用方式的前提下,实现给函数增加额外的功能。
装饰器的本质就是一个闭包函数(三步:① 有嵌套 ② 有引用 ③ 有返回)
2. 装饰器的雏形
# 要求:把登录功能封装起来(比如封装成一个函数,添加这个登录不能影响现有功能函数)
'''
装饰器:本质是一个闭包,有嵌套、有引用、有返回(返回的是函数的内存地址)
参数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()
3. 装饰器定义
'''
装饰器:本质就是一个闭包 ① 有嵌套 ② 有引用 ③ 有返回
'''
def check(fn):
def inner():
# 开发登录验证功能
print('验证登录')
# 执行原有函数
fn()
return inner
@check
def comment():
print('发表评论')
comment()
4. 装饰器的作用:获取程序的执行时间
'''
定义获取程序的执行时间装饰器 => 闭包(① 有嵌套 ② 有引用 ③ 有返回)
'''
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():
for i in range(1000000):
print(i)
demo()
5. 带有参数装饰器
'''
带有参数的装饰器:① 有嵌套 ② 有引用 ③ 有返回
'''
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以元组形式传递,a=30,b=40以字典形式传递
sum_num(10, 20, a=30, b=40)
6. 带有返回装饰器
'''
带有返回值的装饰器:① 有嵌套 ② 有引用 ③ 有返回
如果一个函数执行完毕后,没有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))
7. 通用版本的装饰器(以后所有的装饰器以此为准)
'''
通用装饰器:① 有嵌套 ② 有引用 ③ 有返回 ④ 有不定长参数 ⑤ 有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))
8. 装饰器高级:使用装饰器传递参数
基本语法
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))
8. 扩展:类装饰器
装饰器还有一种特殊的用法就是类装饰器,就是通过定义一个类来装饰函数。
class 类装饰器():
# 装饰器代码
@类装饰器名称
def 函数():
# 函数代码
编写一个Check类装饰器,用于实现用户的权限验证
'''
类装饰器编写规则:
① 必须有一个__init__初始化方法,用于接收要装饰函数的函数
② 必须把这个类转换为可以调用的函数
问题:如何把一个类当做一个装饰器函数进行调用(把类当做函数)
'''
class Check():
def __init__(self, fn):
# fn就是要修饰函数的名称,当Check装饰器类被调用时,系统会自动把comment函数名称传递给fn变量
self.__fn = fn
# __call__方法:把一个类转换为函数的形式进行调用
def __call__(self, *args, **kwargs):
# 编写装饰器代码
print('请先登录')
# 调用comment函数本身
self.__fn(*args, **kwargs)
# 编写一个函数,用于实现评论功能,底层comment = Check(comment)
@Check
def comment():
print('评论功能')
# 调用comment函数,实现评论功能
comment()