Python: 装饰器与闭包

装饰器是 Python 最精妙的特性之一。它让你能在不修改原函数代码的前提下,给函数"加点料"——加日志、计时间、检查权限……很多框架和库都大量使用装饰器。理解它是 Python 进阶的重要里程碑。


1. 闭包

闭包(Closure)是装饰器的基础。简单说:内层函数记住了外层函数的变量,即使外层函数已经执行完了。

PYTHON
def make_multiplier(n):
    """创建一个乘 n 的函数"""
    def multiplier(x):
        return x * n        # 内层函数记住了外层函数的变量 n
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))            # 10
print(triple(5))            # 15
print(double(10))           # 20

doubletriple 虽然来自同一个工厂函数 make_multiplier,但它们各自记住了不同的 n 值(2 和 3)。这就是闭包——函数 + 它记住的外部变量

💡 闭包的应用场景: ① 工厂函数(像上面那样创建不同配置的函数)。② 装饰器(标准实现方式)。③ 回调函数中保存上下文。



2. 装饰器基础

装饰器就是一个接收函数作为参数、返回新函数的函数:

PYTHON
def my_decorator(func):
    def wrapper():
        print("=== 函数执行前 ===")
        func()
        print("=== 函数执行后 ===")
    return wrapper

@my_decorator
def say_hello():
    print("你好!")

say_hello()

运行结果:

TEXT 📖 仅展示
=== 函数执行前 ===
你好!
=== 函数执行后 ===

@my_decorator 等同于 say_hello = my_decorator(say_hello)。语法糖让代码看起来更清爽。

▶ 示例:日志装饰器(难度⭐⭐)

PYTHON
def log_call(func):
    """记录函数调用"""
    def wrapper(*args, **kwargs):
        print(f"[LOG] 调用 {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} 返回 {result}")
        return result
    return wrapper

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

@log_call
def greet(name, greeting="你好"):
    return f"{greeting},{name}!"

print(add(3, 5))
print(greet("Alice"))
▶ 试一试

运行结果:

TEXT 📖 仅展示
[LOG] 调用 add((3, 5), {})
[LOG] add 返回 8
8
[LOG] 调用 greet(('Alice',), {'greeting': '你好'})
[LOG] greet 返回 你好,Alice!


3. functools.wraps:保留原函数信息

装饰器会"替换"原函数,导致函数名和文档丢失。@wraps 修复这个问题:

PYTHON
from functools import wraps

def my_decorator(func):
    @wraps(func)                    # 保留原函数的信息
    def wrapper(*args, **kwargs):
        """装饰器内部函数"""
        print("函数执行前...")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    """一个简单的问候函数"""
    print("你好!")

print(say_hello.__name__)           # say_hello(如果没有 @wraps,会显示 wrapper)
print(say_hello.__doc__)            # 一个简单的问候函数(如果没有 @wraps,会显示装饰器内部函数)
💡 写装饰器时永远加 @wraps。不加的话,被装饰后的函数名字和文档就丢了,调试起来很麻烦。从 functools 导入 wraps,在内部 wrapper 函数上加上 @wraps(func)



4. 带参数的装饰器

有时候装饰器本身也需要参数:

PYTHON
from functools import wraps

def repeat(times=2):
    """重复执行指定次数"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"你好,{name}!")

greet("Alice")

运行结果:

TEXT 📖 仅展示
你好,Alice!
你好,Alice!
你好,Alice!

▶ 示例:计时装饰器(难度⭐⭐⭐)

PYTHON
import time
from functools import wraps

def timer(func):
    """计算函数执行时间"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"[计时] {func.__name__} 耗时:{elapsed:.4f}秒")
        return result
    return wrapper

@timer
def slow_function():
    total = 0
    for i in range(1000000):
        total += i
    return total

result = slow_function()
print(f"结果:{result}")
▶ 试一试

运行结果(时间因电脑而异):

TEXT 📖 仅展示
[计时] slow_function 耗时:0.0452秒
结果:499999500000


▶ 示例:简易计时装饰器(难度⭐)

PYTHON
import time
from functools import wraps

def simple_timer(func):
    """简单的计时装饰器——打印函数执行耗时"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"[{func.__name__}] 耗时 {elapsed:.6f} 秒")
        return result
    return wrapper

@simple_timer
def sum_range(n):
    """计算 1 到 n 的累加和"""
    total = 0
    for i in range(1, n + 1):
        total += i
    return total

@simple_timer
def greet(name):
    """简单问候"""
    return f"你好,{name}!"

result = sum_range(100000)
print(f"累加结果:{result}")
print(greet("Alice"))
▶ 试一试

输出:

TEXT 📖 仅展示
[sum_range] 耗时 0.008123 秒
累加结果:5000050000
[greet] 耗时 0.000001 秒
你好,Alice!


5. 多个装饰器组合

一个函数可以叠加多个装饰器,执行顺序从下到上:

PYTHON
from functools import wraps

def bold(func):
    @wraps(func)
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper

def italic(func):
    @wraps(func)
    def wrapper():
        return f"<i>{func()}</i>"
    return wrapper

@bold
@italic
def hello():
    return "你好"

print(hello())              # <b><i>你好</i></b>
# 执行顺序:先 @italic 再 @bold

# 等价于:hello = bold(italic(hello))
💡 多个装饰器从下往上执行。 想象函数是一层一层"包裹"起来的——最下面的装饰器最靠近原函数,最先执行。理解了这个顺序,多个装饰器组合起来就不会乱了。



6. 常见应用场景


❓ 常见问题

Q 装饰器和闭包是什么关系?
A 装饰器用闭包实现。闭包是"函数记住了外部变量",装饰器正是利用闭包让内层wrapper函数能访问到外层传来的func参数。
Q Python 自带了哪些有用的装饰器?
A @property(属性访问)、@staticmethod(静态方法)、@classmethod(类方法)、@functools.lru_cache()(缓存)、@functools.wraps(保留函数信息)、@dataclass(数据类)。这些都在标准库中。

📖 小节


📝 作业

  1. 基础题(难度⭐):写一个装饰器 print_call,在函数执行时打印"正在调用 xxx 函数"。

  2. 进阶题(难度⭐⭐):写一个装饰器 retry(max_attempts=3),当函数抛出异常时自动重试指定次数。如果重试后仍然失败,抛出最后的异常。提示:在 wrapper 中用 for 循环 + try-except。

  3. 挑战题(难度⭐⭐⭐):写一个带参数的装饰器 cache_result(ttl=60),缓存函数的返回值。在 ttl 秒内调用同一个参数时直接返回缓存结果,超过 ttl 重新计算。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏