在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函数的内存地址,并执行器内部的代码(num=20),在于闭包函数保留了num=20这个局部变量
# 输出结果
20闭包的作用:正常情况下,当执行func()的时候,函数内部的变量num = 20,会随着函数的func函数的结束而被垃圾回收机制所回收。所以闭包的真正作用:就是可以在全局作用域中,实现间接对局部变量进行访问。
4. 注意事项
由于闭包引用了外部函数的变量,所以外部函数的变量并没有及时释放,消耗内存。
5. 在闭包的内部实现对外部变量的修改
错误版本:
'''
python闭包:① 有嵌套 ② 有引用 ③ 有返回
'''
def outer():
num = 10
def inner():
# 这种写法无法实现通过闭包修改外部的局部变量
num = 20
print('outer函数中的num:', num) # 10
inner() # 执行函数inner,让num=20生效
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,让num=20生效
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引用局部变量result=0,然后进行+1操作,弹出0+1=1
继续执行
执行f(2),相当于执行inner函数,声明nonlocal result,代表还是引用外部的局部变量,由于此时外部的result已经被f(1)更改为1了,所以由于局部变量一直没有消失,所以此时result=1,执行+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():
sum=0
for i in range(1000000):
sum+=i
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以元组形式传递,a=30,b=40以字典形式传递
sum_num(10, 20, a=30, b=40)
# 运行结果
-- 日志信息... --
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 可以避免装饰器覆盖原函数的元数据,是一个良好的编程习惯。
到此这篇关于深入理解python闭包与装饰器从入门到进阶的文章就介绍到这了,更多相关python闭包与装饰器内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论