Python进阶系列之-闭包和装饰器

Python进阶系列之-闭包和装饰器

写在前面:在Python进阶的道路上,有两个"拦路虎"让无数小白直呼头秃,它们就是闭包和装饰器。很多同学背了八股文知道它们是什么,但一到实际应用就懵圈。其实,它们并没有那么神秘!

闭包与装饰器是Python函数式编程的核心知识点,也是Python进阶路上的必经之路。装饰器基于闭包实现,可以在不修改原函数源码、不改变原函数调用方式的前提下,为函数扩展额外功能,完美契合软件开发的"开闭原则"。

本文从基础概念出发,逐层拆解闭包的底层原理、装饰器的多种实现写法与执行逻辑,全程配合内存图解与可运行代码,帮你彻底吃透这一面试高频考点。

今日内容大纲:

  1. 函数名的用法(万物皆对象)
  2. 闭包详解
  3. 装饰器详解

文章目录


一、前置知识:函数名也是对象

在讲闭包之前,必须先扭转一个观念:在Python中,万物皆对象。 函数也不例外!函数名本质上是函数对象的内存地址(函数入口),这是闭包和装饰器能够成立的底层基础。

1.1 函数名是对象,支持赋值

函数名存储的是函数对象的引用,因此可以像普通变量一样赋值给其他变量,赋值后的变量等价于原函数,可以直接调用。

我们平时写的函数名(比如 func01),它其实就是一个变量,里面存的是这个函数在内存中的地址(函数入口)。而 func01() 加上括号才表示"调用这个函数"。

python 复制代码
# 需求: 定义1个无返回值的func01(), 并直接输出函数名.
def func01():
    print('hello world!')

if __name__ == '__main__':
    # 1. 直接打印函数名,输出的是内存地址
    print(func01)  # <function func01 at 0x00000288D53E8C10>
    
    # 2. 调用函数
    print(func01()) # 先执行func01打印hello world!,然后打印返回值None
    
    # 3. 赋值操作:把函数名当成对象赋值给另一个变量
    f = func01
    print(f)  # 地址和上面一样,说明f也指向这个函数
    
    # 4. 间接调用
    f()  # 相当于调用了 func01()

1.2 函数名可以作为参数传递

既然函数名是对象,那它当然也能作为参数传递给另一个函数!这种把"函数A传给函数B,并在B中调用A"的写法,在Python中称为回调函数。。

python 复制代码
# 1. 定义1个无参函数 method()
def method():
    print('我是 method 函数')

# 2. 定义1个有参数的函数 func(),接收一个函数对象
def func(fn):
    fn() # 在内部调用传进来的函数

# 3. 调用: 把函数名 method 传给 func
func(method)  # 输出: 我是 method 函数

小白理解:函数就像是一张"菜谱",函数名就是菜谱的名字。把函数名传给别人,就是把菜谱递给别人,别人拿到菜谱后按上面的步骤做菜(加括号调用)。

二、闭包:封装变量的函数嵌套

2.1 什么是闭包?

简单来说,引用了外部函数变量的内部函数,就叫做闭包

闭包最大的作用是:保存函数内的局部变量,让变量不会随着外部函数调用完毕而销毁,延长了局部变量的生命周期。

大白话解释:在一个函数里面又定义了一个函数,而且里面的函数用到了外面函数的变量。这就相当于内部函数把外部变量的值"包"了起来,带走了。

形成闭包必须同时满足三个条件(死记硬背!):

  1. 有嵌套:函数嵌套定义,分为外部函数和内部函数
  2. 有引用:内部函数使用了外部函数的变量(包括形参)
  3. 有返回:外部函数返回内部函数名(函数对象)

标准格式:

python 复制代码
def 外部函数名(形参列表):
    # 外部函数变量
    def 内部函数名(形参列表):
        # 使用外部函数的变量
        ...
    return 内部函数名

2.2 基础闭包示例与执行流程

我们以一个求和闭包为例,拆解闭包的完整执行过程。

python 复制代码
# 需求: 定义用于求和的闭包. 外部函数有参数num1, 内部函数有参数num2.
def fun_outer(num1):
    # 内部函数
    def fun_inner(num2):  # 1. 有嵌套
        # 引用外部函数的变量 num1
        sum_val = num1 + num2  # 2. 有引用 (使用了外部函数的num1)
        print(f'sum的值: {sum_val}')
    # 外部函数返回内部函数对象
    return fun_inner  # 3. 有返回 (返回内部函数名)

# 1. 调用外部函数,得到内部函数的引用
f = fun_outer(10)   # 细节: f就是内部函数对象, 即: f = fun_inner。此时外部的num1=10被保存了下来!
f(1)  # 11 (10 + 1)
f(1)  # 11 (10 + 1)
f(1)  # 11 (10 + 1)
# 为什么输出全是11? 因为每次调用 f(1) 时,外部函数的 num1 始终被闭包保存在内存里,值就是 10。

# 2. 多次调用内部函数,都能访问到外部函数的变量
f(1)    # 11
f(2)    # 12
f(3)    # 13
闭包执行流程内存图解

闭包的执行可以分为5个核心步骤,内存中变量的生命周期如下图所示:

  1. 调用fun_outer(10),在栈中执行外部函数,创建变量num1=10
  2. 外部函数执行完毕,返回内部函数对象的地址,赋值给变量f
  3. 调用f(1),执行内部函数
  4. 内部函数访问外部函数保留的num1,完成计算并输出
  5. 内部函数执行完毕,外部函数的变量依然被闭包保留,不会销毁

2.3 nonlocal关键字:修改外部函数变量

在闭包中,内部函数默认只能读取外部函数的变量,如果要修改外部函数的变量值 ,必须使用nonlocal关键字声明,否则会报错。

对比记忆:

  • global:声明修改全局变量,在整个py文件中生效
  • nonlocal:声明修改外部函数的变量,在闭包嵌套层级中生效
python 复制代码
def fun_outer():  # 有嵌套
    a = 100  # 外部函数的变量
    
    def fun_inner():  # 有嵌套
        # 核心细节: 在内部函数中修改外部函数的变量值, 要通过 nonlocal 关键字实现.
        nonlocal a  
        a = a + 1  # 修改外部变量a
        print(f'a的值为: {a}')  # 有引用
        
    return fun_inner  # 有返回

# 调用
fn = fun_outer() # fn = fun_inner
fn()  # a变成101
fn()  # a变成102
fn()  # a变成103
nonlocal内存图解

使用nonlocal后,内部函数修改的是外部函数同一块内存中的变量,变量状态会持续保留:

2.4 闭包的核心价值

  • 保留函数执行后的状态,让局部变量的生命周期延长
  • 实现数据私有化,外部无法直接访问闭包内的变量
  • 是装饰器实现的底层基础

三、装饰器:无侵入式功能增强

3.1 装饰器的本质与作用

装饰器本质上就是一个闭包函数 ,它的核心作用是:

在不修改原函数源码、不改变原函数调用方式的前提下,为原函数扩展额外功能(比如日志打印、登录校验、性能计时等)。

装饰器相比闭包,多了一个核心特点:接收原函数作为参数,在内部函数中调用原函数,并在前后插入增强逻辑。

大白话解释:装饰器本质上就是闭包的一种写法。它的作用是:在不改变原有函数源码、不改变原有函数调用方式的基础上,给原有函数增加新的功能(做增强)。

前提条件(4点):

1.有嵌套 2. 有引用 3. 有返回 4. 有额外功能

语法糖: 在要被装饰的函数上写 @装饰器名,之后正常调用函数即可,Python会自动帮我们做增强。

3.2 装饰器入门:两种写法

我们以「发表评论前需要先登录」为例,讲解装饰器的两种实现方式。

方式1:传统手动包装写法
python 复制代码
# 1.定义装饰器(无参无返回值)
def check_user(fn):  # fn是要被装饰的函数名
    def inner():
        print('登录中...')  # 4. 有额外功能
        fn()  # 2. 有引用 (调用原函数)
    return inner  # 3. 有返回

# 2. 定义原函数,并使用语法糖
@check_user
def comment():
    print('发表评论!')

# 3. 测试
if __name__ == '__main__':
    comment()  # 直接调用,会先打印"登录中...",再打印"发表评论!"
方式2:语法糖写法(推荐)

Python提供了@装饰器名的语法糖,效果和手动包装完全一致,代码更简洁优雅。

python 复制代码
# 定义装饰器
def check_user(fn):
    def inner():
        print('登录中...')
        fn()
    return inner

# 在原函数上添加语法糖
@check_user
def comment():
    print('发表评论!')

# 直接调用原函数即可,自动生效增强功能
comment()

💡 本质说明

@check_user 写在函数上,等价于执行了 comment = check_user(comment)

3.3 装饰器的四种形态(重点细节)

核心原则: 装饰器的内部函数格式,必须和原函数格式保持一致! 原函数有参数,内部函数也要有;原函数有返回值,内部函数也要返回。

为了省事,我们通常直接写通用装饰器(使用可变参数*args**kwargs接收任意参数),适配所有情况:

python 复制代码
# 通用装饰器写法:使用 *args, **kwargs 接收任意参数
def print_info(fn):
    def inner(*args, **kwargs):  # 适配原函数的各种参数
        print('[友好提示] 正在努力计算中!')  # 额外功能
        result = fn(*args, **kwargs)  # 调用原函数,并接收返回值
        return result  # 返回原函数的执行结果
    return inner

# 测试:有参有返回值的函数
@print_info
def get_sum(*args, **kwargs):
    sum = 0
    for i in args:
        sum += i
    for value in kwargs.values():
        sum += value
    return sum

if __name__ == '__main__':
    print(get_sum(1, 2, 3, a=10, b=20)) # 结果: 36
场景1:装饰无参无返回值函数
python 复制代码
def print_info(fn):
    def inner():
        print('[友好提示] 正在努力计算中!')
        fn()
    return inner

@print_info
def get_sum():
    a = 10
    b = 20
    print(f'sum的值为: {a + b}')

get_sum()
场景2:装饰有参无返回值函数
python 复制代码
def print_info(fn):
    def inner(a, b):
        print('[友好提示] 正在努力计算中!')
        fn(a, b)
    return inner

@print_info
def get_sum(a, b):
    print(f'sum的值: {a + b}')

get_sum(10, 20)
场景3:装饰有参有返回值函数
python 复制代码
def print_info(fn):
    def inner(a, b):
        print('[友好提示] 正在努力计算中!')
        result = fn(a, b)   # 接收原函数返回值
        return result       # 向外返回结果
    return inner

@print_info
def get_sum(a, b):
    return a + b

print(get_sum(1, 2))
场景4:装饰无参有返回值函数
python 复制代码
def print_info(fn):
    def inner():
        print('[友好提示] 正在努力计算中!')
        return fn()
    return inner

@print_info
def get_sum():
    return 10 + 20

print(get_sum())

3.5 多个装饰器的执行顺序

一个函数可以同时被多个装饰器装饰,核心规则:

  • 装饰顺序:由内到外依次包装(先靠近函数的装饰器先生效)
  • 执行顺序:由外到内依次执行(先运行外层装饰器的前置逻辑,再往里走)

以「先登录、再验证码校验」为例:

python 复制代码
# 装饰器1:登录
def check_login(fn):
    def inner():
        print('登录中...')
        fn()
    return inner

# 装饰器2:验证码
def check_code(fn):
    def inner():
        print('校验验证码...')
        fn()
    return inner

# 使用多个语法糖:先验证码,后登录?还是先登录,后验证码?
@check_login   # 第1步:先执行外层
@check_code    # 第2步:后执行内层
def comment():
    print('发表评论!')

if __name__ == '__main__':
    comment()
"""
输出结果:
登录中...
校验验证码...
发表评论!
"""
多装饰器执行顺序图解

多个装饰器的包装与执行流程,可以用下图直观理解:

3.6 带参数的装饰器

普通的装饰器只能传一个参数(就是原函数 fn)。如果希望装饰器可以接收自定义参数,实现差异化的功能增强,就是装饰器本身也需要接收参数怎么办?(比如根据不同的运算符号给出不同的提示)。

结论: 在原来的装饰器外面,再套一层函数!

python 复制代码
# 1. 定义带参数的装饰器
def logging(flag):  # 接收装饰器自身的参数
    def decorator(fn): # 接收原函数
        def inner():
            # 根据传入的flag做不同的额外功能
            if flag == '+':
                print('---[友好提示] 正在努力计算 加法 运算中 ---')
            elif flag == '-':
                print('---[友好提示] 正在努力计算 减法 运算中 ---')
            fn()
        return inner
    return decorator  # 返回真正的装饰器

# 2. 使用带参数的装饰器
@logging('+')  # 先调用logging('+'),返回decorator,然后再用decorator去装饰add函数
def add():
    print('我是加法运算!')

@logging('-')
def substract():
    print('我是减法运算!')

# 3. 测试
if __name__ == '__main__':
    add()
    print('-' * 31)
    substract()

四、全文总结

知识点 核心要义 必要条件/口诀
函数名 是对象,是内存地址 加括号是调用,不加括号是传递对象
闭包 内函数用外函数的变量 有嵌套、有引用、有返回
装饰器 不改原码增强功能 有嵌套、有引用、有返回、有额外功能
通用装饰器 适配所有原函数 内部函数写 *args, **kwargs,并 return fn(...)
多装饰器 装饰多个功能 语法糖从上往下执行
带参装饰器 装饰器自身需要参数 外面再加一层函数,返回真正的装饰器
  1. 函数是对象:Python中函数是对象,函数名是引用,可以赋值、作为参数传递、作为返回值。
  2. 闭包三要素:函数嵌套、内函数引用外函数变量、外函数返回内函数名;核心作用是保留变量状态。
  3. nonlocal关键字 :闭包中修改外部函数变量时必须声明,区别于操作全局变量的global
  4. 装饰器本质:基于闭包实现,无侵入式增强函数功能,符合开闭原则。
  5. 装饰器核心规则 :内部函数的参数、返回值要与原函数对齐;通用装饰器用*args, **kwargs适配所有函数。
  6. 多装饰器顺序:装饰时由内到外,执行时由外到内。
  7. 带参装饰器:外层多一层函数接收自定义参数,返回标准装饰器即可。

🚀 避坑指南 :写装饰器时,最容易犯的错误就是内部函数的格式和原函数不一致 。如果你不确定原函数有没有参数、有没有返回值,请直接使用通用装饰器*args, **kwargsreturn 缺一不可)!

结束语:闭包和装饰器是Python进阶的分水岭、是Python高阶语法的精髓,掌握它们不仅能写出更优雅的代码,也是理解框架源码、应对面试的必备技能。后续学习Django、Flask等Web框架时,装饰器无处不在。多写几遍代码,画画内存图,你会发现不过如此!

如果这篇博客对你有帮助,记得点赞+收藏哦!有任何疑问欢迎在评论区留言交流~