Python 闭包与装饰器:从入门到精通

大家好!今天我们来深入学习 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)
执行顺序 从下往上(靠近函数的先执行)
带参数装饰器 三层嵌套

📝 最后提醒:闭包是装饰器的底层基础,理解闭包才能真正掌握装饰器。多练习、多思考,自然就能灵活运用!

如果觉得有帮助,欢迎点赞、收藏、评论! 😊

相关推荐
老大白菜1 小时前
Qwen3.8-27B 本地推理 + DeepSeek Harness 配置
python·qwen·deepseek·harness
余额瞒着我当琳1 小时前
C++--vector底层,leetcode118杨辉三角实战,对于传统reserve的坑点,迭代器失效
开发语言·c++
曹牧1 小时前
C#:字符串做20位截断
开发语言·c#
Code额2 小时前
Python 连接 DeepSeek API,OpenAI 对话方式总结
后端·python·ai·ai编程
电化学仪器白超2 小时前
MV-CS200-10UC相机参数配置
python·单片机·嵌入式硬件·数码相机·自动化·ltspice
醉城夜风~2 小时前
(超详细)C++多态(Polymorphism)
开发语言·c++
这是个栗子2 小时前
【JS代码分析】前端鉴权基础:Token 的本地存储与状态重置实践
开发语言·前端·javascript
做前端的娜娜子2 小时前
async/await 错误处理:try...catch vs .catch() 完全指南
前端·面试·掘金·金石计划
Scabbards_2 小时前
面试Leetcode - 算法合集
算法·leetcode·面试