目录
前言
在 Python 中,闭包和装饰器是函数式编程的精髓,它们能让代码更简洁、更灵活,尤其在 “不修改原函数” 的前提下为其添加功能时堪称 “神器”。本文从概念到实战,带你吃透这两个看似抽象的知识点。
一、闭包:函数 “记住” 外部变量的魔法
1. 什么是闭包?
闭包是嵌套函数的一种特殊形式:
- 内部函数引用了外部函数的变量(非全局变量);
- 外部函数返回了内部函数。
简单说,闭包让内部函数 “记住” 了它诞生时的环境(外部变量的值)。
2.闭包的基本示例
def outer_func(outer_var):
# 外部函数定义一个变量 outer_var
def inner_func(inner_var):
# 内部函数引用外部变量 outer_var
return outer_var + inner_var
# 外部函数返回内部函数(不加括号,返回函数本身)
return inner_func
# 调用外部函数,得到内部函数对象,此时 outer_var 被“记住”
closure = outer_func(10)
# 调用内部函数,传入 inner_var
print(closure(5)) # 输出:15(10 + 5)
print(closure(8)) # 输出:18(10 + 8)
-
outer_func
调用后,变量outer_var=10
本应随函数结束而销毁,但闭包让inner_func
记住了它的值。 -
每次调用
closure(inner_var)
,都会用 “记住的 10” 和传入的inner_var
计算。
3.闭包的核心作用(保存状态)
闭包最大的价值就是保存外部变量的状态,避免使用全局变量
比如实现一个"计数器":
def make_counter():
count = 0 # 外部变量:记录计数
def counter():
nonlocal count # 声明引用外部变量(非全局)
count += 1
return count
return counter
# 创建两个计数器(彼此独立,状态不共享)
counter1 = make_counter()
counter2 = make_counter()
print(counter1()) # 1(counter1的count变为1)
print(counter1()) # 2(counter1的count变为2)
print(counter2()) # 1(counter2的count独立变为1)
-
两个计数器
counter1
和counter2
分别 “记住” 自己的count
状态,互不干扰,比用全局变量更优雅。
4. 闭包的条件
形成闭包必须满足 3 个条件:
- 存在嵌套函数(内部函数定义在外部函数中);
- 内部函数引用了外部函数的非全局变量;
- 外部函数返回了内部函数。
二、装饰器:基于闭包的 “功能增强” 神器
1. 什么是装饰器?
装饰器本质是一个闭包函数,它的特殊之处在于:
- 接收一个函数作为参数(被装饰的函数);
- 返回一个新的函数(添加了额外功能的 “包装函数”);
- 用
@装饰器名
的语法糖简化调用。
核心作用:在不修改原函数代码的前提下,为其添加额外功能(如日志、计时、权限校验等)。
2. 装饰器的基本用法:给函数 “加功能”
比如,我们想给一个函数添加 “执行前打印日志” 的功能,用装饰器实现如下:
# 1. 定义装饰器(本质是闭包)
def log_decorator(func): # 接收被装饰的函数作为参数
def wrapper(): # 包装函数:添加额外功能 + 调用原函数
print(f"==== 开始执行 {func.__name__} 函数 ====") # 额外功能:打印日志
func() # 调用原函数
print(f"==== {func.__name__} 函数执行结束 ====") # 额外功能:打印日志
return wrapper # 返回包装函数
# 2. 用 @ 语法糖给原函数“戴装饰器”
@log_decorator
def say_hello():
print("Hello, 装饰器!")
# 3. 调用原函数(实际执行的是被装饰后的 wrapper 函数)
say_hello()
输出结果:
==== 开始执行 say_hello 函数 ====
Hello, 装饰器!
==== say_hello 函数执行结束 ====
-
原函数
say_hello
代码未变,但被log_decorator
装饰后,自动拥有了日志功能; -
@log_decorator
等价于say_hello = log_decorator(say_hello)
,这是 Python 的语法糖简化。
3. 装饰器的工作原理
用一张图看懂装饰器的执行流程:
原函数 say_hello → 被装饰器 log_decorator 接收 → 包装成 wrapper 函数(含日志+原函数调用)→ 变量 say_hello 指向 wrapper
当调用 say_hello()
时,实际执行的是 wrapper()
,从而在不修改原函数的情况下 “增强” 了功能。
4. 进阶:装饰带参数的函数
如果原函数有参数(如 add(a, b)
),装饰器需要兼容参数传递。只需让 wrapper
函数接收可变参数 *args
和 **kwargs
即可:
def log_decorator(func):
def wrapper(*args, **kwargs): # 用 *args 接收位置参数,** kwargs 接收关键字参数
print(f"==== 执行 {func.__name__},参数:{args}, {kwargs} ====")
result = func(*args, **kwargs) # 传递参数给原函数
print(f"==== {func.__name__} 返回结果:{result} ====")
return result # 返回原函数的结果
return wrapper
@log_decorator
def add(a, b):
return a + b
# 调用带参数的函数
print(add(3, 5)) # 输出:8
输出结果:
==== 执行 add,参数:(3, 5), {} ====
==== add 返回结果:8 ====
8
-
*args
和**kwargs
让装饰器兼容任意参数的函数,通用性极强。
5. 带参数的装饰器:让装饰器更灵活
有时装饰器需要自己的参数(如日志的级别),这时需要三层嵌套的闭包:最外层接收装饰器参数,中间层接收原函数,最内层是包装函数。
例如,实现一个可指定日志级别的装饰器:
# 带参数的装饰器(三层嵌套)
def log_level(level): # 最外层:接收装饰器参数(日志级别)
def log_decorator(func): # 中间层:接收原函数
def wrapper(*args, **kwargs): # 最内层:包装函数
print(f"[{level}] 执行 {func.__name__},参数:{args}")
result = func(*args, **kwargs)
return result
return wrapper
return log_decorator
# 用不同参数装饰函数
@log_level("INFO") # 等价于 @log_decorator(log_decorator 由 log_level("INFO") 返回)
def multiply(a, b):
return a * b
@log_level("WARNING")
def divide(a, b):
return a / b
print(multiply(2, 3)) # 输出:6
print(divide(6, 2)) # 输出:3.0
输出结果:
[INFO] 执行 multiply,参数:(2, 3)
6
[WARNING] 执行 divide,参数:(6, 2)
3.0
-
带参数的装饰器本质是 “装饰器工厂”:先调用
log_level("INFO")
生成一个装饰器,再用这个装饰器装饰函数。
6. 修复原函数的 “身份信息”
装饰器会让原函数的元信息(如 __name__
、__doc__
)被替换成 wrapper
的信息,例如:
def decorator(func):
def wrapper():
func()
return wrapper
@decorator
def test():
"""这是test函数的文档字符串"""
pass
print(test.__name__) # 输出:wrapper(本该是 test)
print(test.__doc__) # 输出:None(本该是“这是test函数的文档字符串”)
解决方法:用 functools.wraps
保留原函数信息:
import functools
def decorator(func):
@functools.wraps(func) # 修复元信息
def wrapper():
func()
return wrapper
@decorator
def test():
"""这是test函数的文档字符串"""
pass
print(test.__name__) # 输出:test(正确)
print(test.__doc__) # 输出:这是test函数的文档字符串(正确)
-
开发装饰器时,建议总是用
functools.wraps
修复元信息,避免调试时 confusion。
三、闭包与装饰器的关系:装饰器是闭包的 “特殊应用”
- 闭包的核心是 “内部函数记住外部变量”;
- 装饰器是闭包的一种特殊形式:外部函数接收的变量是 “被装饰的函数”,内部函数(wrapper)调用该函数并添加功能。
可以说,装饰器是为函数增强功能而设计的闭包。
四、实际应用场景:装饰器的 “超能力”
装饰器在实际开发中无处不在,以下是几个典型场景:
1. 日志记录
自动记录函数的调用时间、参数、返回值,方便调试和监控:
import time
import functools
def log_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 执行耗时:{end - start:.2f}秒")
return result
return wrapper
@log_time
def slow_func():
time.sleep(1) # 模拟耗时操作
slow_func() # 输出:slow_func 执行耗时:1.00秒
2. 权限验证
在函数执行前检查用户权限,无权限则拒绝执行:
def check_permission(func):
@functools.wraps(func)
def wrapper(user):
if user == "admin":
return func(user)
else:
return "权限不足,无法执行"
return wrapper
@check_permission
def delete_data(user):
return f"{user} 成功删除数据"
print(delete_data("admin")) # 输出:admin 成功删除数据
print(delete_data("guest")) # 输出:权限不足,无法执行
3. 缓存结果(减少重复计算)
对耗时的函数(如计算斐波那契数列),缓存已计算的结果,避免重复运算:
def cache(func):
cache_dict = {} # 闭包变量:缓存计算结果
@functools.wraps(func)
def wrapper(n):
if n not in cache_dict:
cache_dict[n] = func(n) # 未缓存则计算并保存
return cache_dict[n] # 直接返回缓存结果
return wrapper
@cache
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print(fib(100)) # 用了缓存,瞬间出结果(否则会很慢)
总结
-** 闭包 **:让函数 “记住” 外部变量,是实现状态保存的利器(如计数器、缓存);
-** 装饰器 **:基于闭包,专注于 “不修改原函数” 地增强功能(如日志、权限、缓存);
- 核心思想:利用函数的嵌套和变量作用域,实现代码的 “模块化” 和 “复用”。
掌握闭包和装饰器,能让你的 Python 代码更具 “Pythonic” 风格 —— 简洁、灵活、可扩展。从今天起,试着用装饰器优化你的函数吧!