深入理解Python闭包与装饰器:从入门到进阶

目录

一、全局变量和局部变量

[1. 作用域](#1. 作用域)

[2. 生命周期](#2. 生命周期)

[3. 全局变量与局部变量的访问范围](#3. 全局变量与局部变量的访问范围)

[4. 问题:为什么在全局作用域中无法访问局部变量呢?](#4. 问题:为什么在全局作用域中无法访问局部变量呢?)

二、闭包

[1. 闭包的概念和作用](#1. 闭包的概念和作用)

2.闭包的格式

[3. 闭包需要满足三个条件](#3. 闭包需要满足三个条件)

[4. 注意事项](#4. 注意事项)

[5. 在闭包的内部实现对外部变量的修改](#5. 在闭包的内部实现对外部变量的修改)

[6. 闭包的综合案例](#6. 闭包的综合案例)

三、装饰器入门

[1. 什么是装饰器](#1. 什么是装饰器)

[2. 格式](#2. 格式)

[3. 装饰器雏形(传统方式)](#3. 装饰器雏形(传统方式))

[4. 装饰器定义(语法糖方式)](#4. 装饰器定义(语法糖方式))

[5. 装饰器案例:获取程序的执行时间](#5. 装饰器案例:获取程序的执行时间)

四、装饰器进阶

[1. 带有参数装饰器](#1. 带有参数装饰器)

[2. 带有返回值装饰器](#2. 带有返回值装饰器)

[3. 通用版本的装饰器](#3. 通用版本的装饰器)

[4. 装饰器高级:使用装饰器传递参数](#4. 装饰器高级:使用装饰器传递参数)

五、补充内容

[1. 多个装饰器的执行顺序](#1. 多个装饰器的执行顺序)

[2. 使用 functools.wraps 保留原函数信息](#2. 使用 functools.wraps 保留原函数信息)

[3. 类装饰器](#3. 类装饰器)

[4. 装饰器的典型应用场景](#4. 装饰器的典型应用场景)

六、总结

在Python编程中,闭包和装饰器是两个紧密相连且非常强大的特性。闭包是实现装饰器的基础,而装饰器则是对闭包的经典应用。在学习它们之前,我们先再重温一下全局变量和局部变量。

一、全局变量和局部变量

1. 作用域

全局变量是函数内外都能访问,局部变量是只能在函数内访问。

2. 生命周期

全局变量伴随着当前主程序的调用而创建,伴随着主程序的结束而销毁。

局部变量伴随着当前所在函数的调用而创建,伴随着函数的结束而销毁。

3. 全局变量与局部变量的访问范围

① 在全局作用域中可以访问全局变量,在局部作用域中可以访问局部变量

python 复制代码
# 全局作用域(全局变量)
num1 = 10
def func():
    # 局部作用域(局部变量)
    num2 = 20
    # 在局部访问局部变量
    print(num2)    #20

# 在全局访问全局变量
print(num1)    #10
# 调用函数
func()

② 在局部作用域中可以访问全局变量

python 复制代码
# 全局作用域(全局变量)
num1 = 10
def func():
    # 局部作用域(局部变量)
    # 在局部作用域中可以访问全局变量
    print(num1)    #10

# 调用函数
func()

③ 在全局作用域中不能访问局部变量

python 复制代码
# 全局作用域(全局变量)
num1 = 10
def func():
    # 局部作用域(局部变量)
    num2 = 20

# 调用函数
func()
# 在全局作用域中调用局部变量num2
print(num2)    # 报错

4. 问题:为什么在全局作用域中无法访问局部变量呢?

答:主要原因在于,在Python的底层存在一个"垃圾回收机制",主要的作用就是回收内存空间,加快计算机的运行。我们在Python代码中定义的变量也是需要占用内存的,所以Python为了回收已经被使用过的内存,会自动将函数运行以后的内部变量和程序直接回收。

当调用完函数后,函数内定义的变量就销毁了,那么如何让局部变量再多待一会,不立刻销毁呢?即可不可以改变函数内变量的生命周期呢?

答案是肯定的---------可以通过闭包来实现

二、闭包

1. 闭包的概念和作用

​概念:在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数称为闭包。

作用:闭包可以保存函数内的变量,而不会随着调用完函数而被销毁。

2.闭包的格式

python 复制代码
def 外部函数名(局部变量):
    def 内部函数名():
        # 内部函数使用了外部函数的局部变量
    return 内部函数地址

3. 闭包需要满足三个条件

  • 有嵌套:外部函数内嵌套了内部函数
  • 有引用:内部函数用了外部函数中的局部变量
  • 有返回:外部函数返回了内部函数名(实际上就是返回内部函数的地址)
python 复制代码
'''
闭包程序三步走: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 复制代码
'''
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 复制代码
'''
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: 20

global和nonlocal核心区别:global定义全局变量,在任意函数内修改全局变量。onlocal在有嵌套函数的前提下,只能在内部函数中修改外部函数的局部变量。

6. 闭包的综合案例

闭包的作用:可以在全局作用域中间接访问局部变量(在函数执行以后)

python 复制代码
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. 格式

python 复制代码
def 外部函数名(局部变量):
    def 内部函数名():
        # TODO 在不改变原始函数基础上,添加额外功能
        内部函数使用了外部函数的局部变量

    return 内部函数地址

3. 装饰器雏形(传统方式)

语法:变量名 = 装饰器名(原有函数名)

变量名()

假设我们有一个评论功能,需要先登录才能执行。我们可以用闭包包装一下:

python 复制代码
# 要求:把登录功能封装起来(比如封装成一个函数,添加这个登录不能影响现有功能函数)
'''
装饰器:本质是一个闭包,有嵌套、有引用、有返回(返回的是函数的内存地址)
参数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. 装饰器定义(语法糖方式)

语法:@装饰器名

python 复制代码
def check(fn):

    def inner():
        # 开发登录验证功能
        print('验证登录')
        # 执行原有函数
        fn()
    return inner

@check
def comment():
    print('发表评论')

comment()

# 运行结果
验证登录
发表评论

5. 装饰器案例:获取程序的执行时间

python 复制代码
# 定义获取程序的执行时间装饰器 
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. 带有参数装饰器

python 复制代码
'''
带有参数的装饰器:① 有嵌套 ② 有引用 ③ 有返回
'''
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)

# 运行结果
-- 日志信息... --
100

2. 带有返回值装饰器

python 复制代码
'''
带有返回值的装饰器:① 有嵌套 ② 有引用 ③ 有返回
如果一个函数执行完毕后,没有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))

# 运行结果
-- 日志信息... --
10

3. 通用版本的装饰器

python 复制代码
'''
通用装饰器:① 有嵌套 ② 有引用 ③ 有返回 ④ 有不定长参数 ⑤ 有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
-- 正在努力计算 --
60

4. 装饰器高级:使用装饰器传递参数

注意:装饰器一次只能接收一个参数

基本语法:

python 复制代码
def 装饰器(fn):
    ...

@装饰器('参数')
def 函数():
    # 函数代码

实例代码:根据传递参数不同,打印不同的日志信息

python 复制代码
'''
通用装饰器:① 有嵌套 ② 有引用 ③ 有返回 ④ 有不定长参数 ⑤ 有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 复制代码
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 保留原函数信息

装饰器会覆盖原函数的 namedoc 等属性。使用 @wraps 可以解决这个问题。

python 复制代码
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 复制代码
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 可以避免装饰器覆盖原函数的元数据,是一个良好的编程习惯。

相关推荐
网易独家音乐人Mike Zhou2 小时前
【Python】TXT、BIN文件的十六进制相互转换小程序
python·单片机·mcu·小程序·嵌入式·ti毫米波雷达
嵌入式学习菌2 小时前
内网穿透全闭环实操指南
linux·开发语言·php
long_songs2 小时前
Python编程第02课:Windows/Mac/Linux环境安装配置详解
windows·python·macos
别抢我的锅包肉2 小时前
【Python】PySpark 核心实操入门指南
python
cyhysr2 小时前
Python读取ppt文本转存txt
python·powerpoint
两年半的个人练习生^_^2 小时前
ThreadLocal的使用和源码
java·开发语言
A懿轩A2 小时前
【NotebookLM 使用教程】 Slides 指令库:11套通用PPT风格Prompt(含中英文对照)
python·prompt·powerpoint·ppt·notebooklm
weixin_408099672 小时前
文字识别通用OCR接口调用与功能说明
图像处理·人工智能·后端·python·ocr·api·文字识别
Thomas.Sir2 小时前
第八章:RAG知识库开发之【Dify 实现数据库数据智能查询系统:从零构建企业级自然语言查询助手】
数据库·python·ai·dify