前言

如果你已经熟悉了函数的基本用法,那么恭喜你来到Python进阶的大门!闭包(Closure)和装饰器(Decorator)是Python函数式编程的精髓,也是面试中绕不开的高频考点。本文将深入剖析闭包的“三要素”,并带你手写通用的日志装饰器、计时装饰器,让你真正理解@语法糖背后的原理。


1. 闭包(Closure)—— 函数工厂的基石

1.1 什么是闭包?

闭包本质上是一个嵌套函数,它让内层函数捕获并记住了外层函数的局部变量,即使外层函数已经执行完毕。

1.2 闭包三要素(面试必背)

  1. 函数嵌套:内层函数定义在外层函数内部。

  2. 内层函数使用外层函数的变量(捕获状态)。

  3. 外层函数返回内层函数(把“行为”返回出去)。

1.3 经典案例:计数器工厂

def counter(step):
    count = 0  # 外部局部变量
    def inner():
        nonlocal count  # 声明修改外部变量
        count += step
        return count
    return inner

# 创建两个独立的计数器实例
c1 = counter(1)
c2 = counter(2)

print(c1())  # 1
print(c1())  # 2
print(c2())  # 2
print(c2())  # 4

每次调用counter,都会生成一个新的闭包空间,c1c2count互不干扰。


2. 装饰器(Decorator)—— 不修改代码,只增强功能

2.1 装饰器的本质

装饰器就是一个闭包,它接收一个函数作为参数,返回一个增强后的新函数。
语法糖 @deco 等价于 func = deco(func)

2.2 第一个计时装饰器

在不改动原函数的情况下,计算函数执行时间:

import time

def timer(func):
    def wrapper():
        start = time.time()
        func()
        print(f"函数 {func.__name__} 耗时: {time.time() - start:.4f}s")
    return wrapper

@timer
def sleep_func():
    time.sleep(1)

sleep_func()  # 函数 sleep_func 耗时: 1.0002s

2.3 通用装饰器(支持参数和返回值)

实际开发中,原函数往往有参数和返回值。我们需要用 *args, **kwargs 通吃一切。

def universal_deco(func):
    def wrapper(*args, **kwargs):
        print(f"正在执行 {func.__name__}")
        result = func(*args, **kwargs)  # 接收返回值
        print("执行结束")
        return result                   # 返回返回值
    return wrapper

@universal_deco
def add(a, b):
    return a + b

print(add(5, 3))  # 8 (同时打印了日志)

3. 多个装饰器叠加(执行顺序)

当多个装饰器叠在一起时,从下往上包装,调用时从上往下执行。

def deco1(func):
    def wrapper():
        print("开始装饰1")
        func()
        print("结束装饰1")
    return wrapper

def deco2(func):
    def wrapper():
        print("开始装饰2")
        func()
        print("结束装饰2")
    return wrapper

@deco1
@deco2
def core():
    print("核心业务")

core()
# 输出顺序:
# 开始装饰1
# 开始装饰2
# 核心业务
# 结束装饰2
# 结束装饰1

4. 实战案例(一):排序算法时间开销统计

我们生成10000个随机数,对比 sorted()list.sort()、冒泡排序、选择排序的效率。利用装饰器,零侵入地统计每个算法耗时。

import random
import time

# 准备4份相同的数据
data = [random.randint(1, 100000) for _ in range(10000)]
d1, d2, d3, d4 = data.copy(), data.copy(), data.copy(), data.copy()

def time_calc(func):
    def wrapper():
        start = time.time()
        func()
        print(f"{func.__name__} 耗时: {time.time() - start:.4f}s")
    return wrapper

@time_calc
def test_sorted():
    sorted(d1)

@time_calc
def test_sort():
    d2.sort()

@time_calc
def test_bubble():
    # 冒泡排序(优化版)
    for i in range(len(d3)-1):
        for j in range(len(d3)-i-1):
            if d3[j] > d3[j+1]:
                d3[j], d3[j+1] = d3[j+1], d3[j]

@time_calc
def test_selection():
    # 选择排序
    for i in range(len(d4)-1):
        min_idx = i
        for j in range(i+1, len(d4)):
            if d4[j] < d4[min_idx]:
                min_idx = j
        if min_idx != i:
            d4[i], d4[min_idx] = d4[min_idx], d4[i]

test_sorted()    # 极快
test_sort()      # 极快
test_bubble()    # 龟速(慎用,可能要几十秒)
test_selection() # 较慢

运行后你会直观感受到算法时间复杂度带来的性能差异,这正是装饰器在性能监控中的典型应用。


5. 实战案例(二):日志记录装饰器(保存到文件)

利用 datetime 记录函数执行的时间、参数和返回值,并写入TXT日志。

from datetime import datetime

def logger(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        # 拼接日志内容
        log_msg = (f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | "
                   f"函数: {func.__name__} | "
                   f"参数: {args} {kwargs} | "
                   f"返回值: {result}\n")
        # 写入文件(追加模式)
        with open("func_log.txt", "a", encoding="utf-8") as f:
            f.write(log_msg)
        return result
    return wrapper

@logger
def max_value(a, b):
    return a if a > b else b

max_value(10, 20)
max_value(100, 50)

打开同目录下的 func_log.txt,你会看到清晰的调用记录。


6. 装饰器带参数(三层嵌套)

有时我们希望装饰器本身能接收参数(如指定日志文件名),需要再包一层。

def file_logger(filename):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            with open(filename, "a") as f:
                f.write(f"{func.__name__} 执行完毕\n")
            return result
        return wrapper
    return decorator

@file_logger("app.log")
def run_task():
    print("任务进行中...")

run_task()

这种“装饰器工厂”写法在实际框架(如Flask路由)中非常常见。


结语

闭包和装饰器是Python进阶路上绕不开的两座大山。理解闭包的三要素,你就能看懂一切装饰器的源码;掌握装饰器的嵌套机制,你就能写出高可用的日志、权限、缓存、计时等通用组件。

建议你亲手把文中的代码敲一遍,观察控制台输出和文件写入结果。只有动手实践,才能真正内化这些知识!

如果这篇文章帮到了你,欢迎一键三连,我们下期见!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐