Python进阶系列之-闭包和装饰器
写在前面:在Python进阶的道路上,有两个"拦路虎"让无数小白直呼头秃,它们就是闭包和装饰器。很多同学背了八股文知道它们是什么,但一到实际应用就懵圈。其实,它们并没有那么神秘!
闭包与装饰器是Python函数式编程的核心知识点,也是Python进阶路上的必经之路。装饰器基于闭包实现,可以在不修改原函数源码、不改变原函数调用方式的前提下,为函数扩展额外功能,完美契合软件开发的"开闭原则"。
本文从基础概念出发,逐层拆解闭包的底层原理、装饰器的多种实现写法与执行逻辑,全程配合内存图解与可运行代码,帮你彻底吃透这一面试高频考点。
今日内容大纲:
- 函数名的用法(万物皆对象)
- 闭包详解
- 装饰器详解
文章目录
- Python进阶系列之-闭包和装饰器
-
- 一、前置知识:函数名也是对象
-
- [1.1 函数名是对象,支持赋值](#1.1 函数名是对象,支持赋值)
- [1.2 函数名可以作为参数传递](#1.2 函数名可以作为参数传递)
- 二、闭包:封装变量的函数嵌套
-
- [2.1 什么是闭包?](#2.1 什么是闭包?)
- [2.2 基础闭包示例与执行流程](#2.2 基础闭包示例与执行流程)
- [2.3 nonlocal关键字:修改外部函数变量](#2.3 nonlocal关键字:修改外部函数变量)
- [2.4 闭包的核心价值](#2.4 闭包的核心价值)
- 三、装饰器:无侵入式功能增强
-
- [3.1 装饰器的本质与作用](#3.1 装饰器的本质与作用)
- [3.2 装饰器入门:两种写法](#3.2 装饰器入门:两种写法)
- [3.3 装饰器的四种形态(重点细节)](#3.3 装饰器的四种形态(重点细节))
- [3.5 多个装饰器的执行顺序](#3.5 多个装饰器的执行顺序)
- [3.6 带参数的装饰器](#3.6 带参数的装饰器)
- 四、全文总结
一、前置知识:函数名也是对象
在讲闭包之前,必须先扭转一个观念:在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 什么是闭包?
简单来说,引用了外部函数变量的内部函数,就叫做闭包 。
闭包最大的作用是:保存函数内的局部变量,让变量不会随着外部函数调用完毕而销毁,延长了局部变量的生命周期。
大白话解释:在一个函数里面又定义了一个函数,而且里面的函数用到了外面函数的变量。这就相当于内部函数把外部变量的值"包"了起来,带走了。
形成闭包必须同时满足三个条件(死记硬背!):
- 有嵌套:函数嵌套定义,分为外部函数和内部函数
- 有引用:内部函数使用了外部函数的变量(包括形参)
- 有返回:外部函数返回内部函数名(函数对象)
标准格式:
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个核心步骤,内存中变量的生命周期如下图所示:

- 调用
fun_outer(10),在栈中执行外部函数,创建变量num1=10 - 外部函数执行完毕,返回内部函数对象的地址,赋值给变量
f - 调用
f(1),执行内部函数 - 内部函数访问外部函数保留的
num1,完成计算并输出 - 内部函数执行完毕,外部函数的变量依然被闭包保留,不会销毁
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(...) |
| 多装饰器 | 装饰多个功能 | 语法糖从上往下执行 |
| 带参装饰器 | 装饰器自身需要参数 | 外面再加一层函数,返回真正的装饰器 |
- 函数是对象:Python中函数是对象,函数名是引用,可以赋值、作为参数传递、作为返回值。
- 闭包三要素:函数嵌套、内函数引用外函数变量、外函数返回内函数名;核心作用是保留变量状态。
- nonlocal关键字 :闭包中修改外部函数变量时必须声明,区别于操作全局变量的
global。 - 装饰器本质:基于闭包实现,无侵入式增强函数功能,符合开闭原则。
- 装饰器核心规则 :内部函数的参数、返回值要与原函数对齐;通用装饰器用
*args, **kwargs适配所有函数。 - 多装饰器顺序:装饰时由内到外,执行时由外到内。
- 带参装饰器:外层多一层函数接收自定义参数,返回标准装饰器即可。
🚀 避坑指南 :写装饰器时,最容易犯的错误就是内部函数的格式和原函数不一致 。如果你不确定原函数有没有参数、有没有返回值,请直接使用通用装饰器 (
*args, **kwargs和return缺一不可)!
结束语:闭包和装饰器是Python进阶的分水岭、是Python高阶语法的精髓,掌握它们不仅能写出更优雅的代码,也是理解框架源码、应对面试的必备技能。后续学习Django、Flask等Web框架时,装饰器无处不在。多写几遍代码,画画内存图,你会发现不过如此!
如果这篇博客对你有帮助,记得点赞+收藏哦!有任何疑问欢迎在评论区留言交流~