大家好!今天我们来深入学习 Python 中两个紧密相关的高级特性:闭包(Closure) 和 装饰器(Decorator)。闭包是装饰器的底层基础,理解闭包才能彻底掌握装饰器。这篇文章将带你从原理到实战,全面掌握这两个知识点。
📚 目录
-
一、global 关键字
-
二、闭包(Closure)
-
三、装饰器原理
-
四、装饰器语法糖
-
五、带参数的装饰器
-
六、wraps 保留元信息
-
七、多个装饰器叠加
-
八、装饰器带参数
-
九、总结
一、global 关键字
1.1 基本用法
global 关键字用于在函数内部声明变量来自全局作用域,从而实现对全局变量的修改。
python
base = 0
def sum_add(num):
global base # 声明 base 是全局变量
base += num
print(base)
sum_add(10) # 10
sum_add(10) # 20
sum_add(10) # 30
1.2 为什么不推荐使用 global?
-
❌ 破坏函数的封装性
-
❌ 让代码难以追踪和维护
-
❌ 容易造成意外的副作用
最佳实践:函数应该尽量独立,通过参数和返回值来传递数据,而不是依赖全局变量。
二、闭包(Closure)
2.1 什么是闭包
官方定义:内部函数引用外部函数的变量。
通俗理解 :闭包让本该被垃圾回收(GC)的变量永远留在内存中,为内部函数所使用。
python
def fun_outer():
num = 0
def fun_inner():
nonlocal num # 声明 num 不是局部变量,向上查找
num += 10
return num
return fun_inner # 返回内部函数的引用
# f 指向 fun_inner 函数
f = fun_outer()
print(f()) # 10
print(f()) # 20
print(f()) # 30
2.2 闭包的形成条件
| 条件 | 说明 |
|---|---|
| 1. 函数嵌套 | 外部函数内部定义了内部函数 |
| 2. 返回内部函数 | 外部函数返回内部函数的引用 |
| 3. 引用外部变量 | 内部函数使用了外部函数的变量 |
4. 使用 nonlocal |
声明该变量不是局部变量,向上查找 |
2.3 释放闭包
当不再需要闭包时,可以手动释放:
python
del f # 删除函数引用
f = None # 置空,让垃圾回收器回收
2.4 经典面试题:函数柯里化
python
"""
实现 add(1) -> 1
add(1)(2) -> 3
add(1)(2)(3) -> 6
add(1)(2)(3)(10) -> 16
...
"""
def add_fun(num):
num_list = [num]
def inner_add(val=None):
if val is None:
return sum(num_list)
num_list.append(val)
return inner_add
return inner_add
print(add_fun(1)(2)(3)(4)(5)(1000)()) # 1015
三、装饰器原理
3.1 装饰器本质上就是闭包
装饰器 = 闭包 + 接收函数作为参数
python
def log(fn):
def inner(a, b):
print("1. 方法开始执行")
result = fn(a, b)
print("2. 方法执行完毕")
return result
return inner
def add_sum(a, b):
return a + b
# 传统方式:手动包装
f = log(add_sum)
print(f(1, 2)) # 3
3.2 装饰器的核心条件
| 条件 | 说明 |
|---|---|
| 1. 有嵌套函数 | 装饰器内部定义了内层函数 |
| 2. 返回内部函数 | 返回内层函数的引用 |
| 3. 引用外部函数参数 | 内层函数使用了被装饰的函数 |
| 4. 增加额外功能 | 在调用原函数前后插入逻辑 |
3.3 经典案例:登录检查装饰器
python
def check_login(fn):
def inner(val):
print("检查当前用户是否登录!!!")
fn(val)
return inner
def publish_comment(content):
print(f"我发表了:{content}")
# 手动装饰
f = check_login(publish_comment)
f("这才是真正的装饰器")
四、装饰器语法糖
4.1 传统写法 vs 语法糖
传统写法:
python
def log(fn):
def inner():
print("日志记录")
fn()
return inner
def common():
print("普通函数")
f = log(common) # 手动包装
f()
语法糖写法(推荐):
python
def log(fn):
def inner():
print("日志记录")
fn()
return inner
@log # 语法糖,等价于 common = log(common)
def common():
print("普通函数")
common() # 直接调用,自动拥有装饰器功能
4.2 不带参数、不带返回值的装饰器
python
def check_login(fn):
def inner():
account = input("请输入您的账号:")
password = input("请输入您的密码:")
if account == "admin" and password == "123456":
fn()
else:
print("用户名或密码错误")
return inner
@check_login
def publish_comment():
print("发表评论成功!")
publish_comment()
五、带参数的通配装饰器
使用 *args 和 **kwargs 让装饰器适配任意参数的函数。
python
def log(fn):
def inner(*args, **kwargs):
print("开始执行某函数")
res = fn(*args, **kwargs)
print("某函数执行完毕")
return res
return inner
@log
def calc_all(*args, **kwargs):
return sum(args) + sum(kwargs.values())
@log
def sum_add(a, b):
return a + b
print(calc_all(1, 2, 3, 4, age=18, money=2000))
print(sum_add(1, 2))
六、wraps 保留元信息
装饰器会丢失 原函数的元信息(如 __name__、__doc__ 等)。使用 @wraps 可以保留这些信息。
python
from functools import wraps
def check_code(fn):
@wraps(fn) # ✅ 保留原函数的元信息
def inner(*args, **kwargs):
code = input("请输入您的验证码:")
if code == "1234":
return fn(*args, **kwargs)
else:
print("验证码错误")
return inner
@check_code
def login():
"""用户登录函数"""
print("用户开始登录")
# 即使被装饰,依然保留原始信息
print(login.__name__) # login(没有 @wraps 会变成 inner)
print(login.__doc__) # 用户登录函数
七、多个装饰器叠加
7.1 执行顺序
多个装饰器叠加时,从下往上执行(靠近函数的先执行)。
python
from functools import wraps
def log(fn):
@wraps(fn)
def inner(*args, **kwargs):
print(f"{fn.__name__}: 开始执行")
res = fn(*args, **kwargs)
print(f"{fn.__name__}: 执行完毕")
return res
return inner
def check_user(fn):
@wraps(fn)
def inner(*args, **kwargs):
user_name = input("请输入您的用户名:")
if user_name == 'admin':
return fn(*args, **kwargs)
else:
print("用户名输入错误")
return inner
def check_code(fn):
@wraps(fn)
def inner(*args, **kwargs):
code = input("请输入您的验证码:")
if code == '1234':
return fn(*args, **kwargs)
else:
print("验证码输入错误")
return inner
# 执行顺序:log → check_user → check_code(从下往上)
@log
@check_user
@check_code
def publish(user_name, content):
print(f"{user_name}: 发表内容: {content}")
publish("admin", "AI时代人人可编程")
八、装饰器带参数
当装饰器本身需要接收参数时,需要三层嵌套。
python
import time
def check_time_out(name, ms=False):
def check_time(fn):
def inner(*args, **kwargs):
begin = time.time()
res = fn(*args, **kwargs)
end = time.time()
cost = end - begin
if ms:
print(f'{name}程序耗时: {cost * 1000:.2f} 毫秒')
else:
print(f'{name}程序耗时: {cost:.2f} 秒')
return res
return inner
return check_time
@check_time_out("zhangsan", ms=True)
def big_loop():
total = 0
for i in range(1000000):
total += i
big_loop()
九、总结
9.1 闭包 vs 装饰器
| 概念 | 核心要点 |
|---|---|
| 闭包 | 内部函数 + 外部变量 + nonlocal |
| 装饰器 | 闭包 + 接收函数作为参数 + 语法糖 @ |
9.2 装饰器核心知识点速记
| 知识点 | 要点 |
|---|---|
| 闭包条件 | 嵌套 + 返回 + 引用外部变量 + nonlocal |
| 装饰器本质 | 闭包,接收函数,返回增强后的函数 |
| 语法糖 | @装饰器名 简化写法 |
| 通配参数 | *args 和 **kwargs |
| 保留元信息 | @wraps(fn) |
| 执行顺序 | 从下往上(靠近函数的先执行) |
| 带参数装饰器 | 三层嵌套 |
📝 最后提醒:闭包是装饰器的底层基础,理解闭包才能真正掌握装饰器。多练习、多思考,自然就能灵活运用!
如果觉得有帮助,欢迎点赞、收藏、评论! 😊