认识装饰器

装饰器(Decorator)是 Python 中一种强大的元编程工具。简单来说,装饰器就是一个函数,它接受另一个函数作为参数,并返回一个新的函数。装饰器允许我们在不修改原函数源代码的情况下,给函数添加额外的功能。

函数是一等公民

要理解装饰器,先要理解 Python 中"函数是一等公民"这个概念:

# 函数可以赋值给变量
def greet(name):
    return f"你好, {name}!"

say_hello = greet
print(say_hello("小明"))  # 输出: 你好, 小明!

# 函数可以作为参数传递
def call_twice(func, arg):
    return func(arg), func(arg)

result1, result2 = call_twice(greet, "小红")
print(result1)  # 输出: 你好, 小红!

# 函数可以嵌套定义
def outer():
    def inner():
        return "我是内部函数"
    return inner

inner_func = outer()
print(inner_func())  # 输出: 我是内部函数

最简单的装饰器

基于上述特性,我们可以写出最简单的装饰器:

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

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

# 手动应用装饰器
say_hello = my_decorator(say_hello)
say_hello()
# 输出:
# 函数调用前执行
# 你好!
# 函数调用后执行

@ 语法糖

Python 提供了 @ 语法来简化装饰器的应用:

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

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

# 等同于: say_hello = my_decorator(say_hello)
say_hello()

@my_decorator 只是语法糖,它的作用完全等同于手动调用 my_decorator(say_hello)

装饰带参数的函数

上面的装饰器无法处理带参数的函数,我们需要让 wrapper 接受任意参数:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"调用函数: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"函数返回: {result}")
        return result
    return wrapper

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

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

print(add(3, 5))
# 输出:
# 调用函数: add
# 函数返回: 8
# 8

print(greet("小明"))
# 输出:
# 调用函数: greet
# 函数返回: 你好, 小明!
# 你好, 小明!

使用 functools.wraps 保留元信息

装饰后的函数会丢失原来的元信息(函数名、文档字符串等):

def my_decorator(func):
    def wrapper(*args, **kwargs):
        """包装函数"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def add(a, b):
    """计算两个数的和"""
    return a + b

print(add.__name__)   # 输出: wrapper(不是 add)
print(add.__doc__)    # 输出: 包装函数(不是 计算两个数的和)

使用 @functools.wraps 可以解决这个问题:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """包装函数"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def add(a, b):
    """计算两个数的和"""
    return a + b

print(add.__name__)   # 输出: add
print(add.__doc__)    # 输出: 计算两个数的和

规则: 每次编写装饰器时,都应该在 wrapper 函数上使用 @functools.wraps

带参数的装饰器

有时我们需要装饰器本身接受参数。这需要再嵌套一层函数:

import functools

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

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

say_hello("小明")
# 输出:
# 你好, 小明!
# 你好, 小明!
# 你好, 小明!

理解这个结构:repeat(times=3) 返回 decorator 函数,@decorator 再将 say_hello 函数作为参数传入,返回 wrapper 函数。

可选参数的装饰器

如果希望装饰器既可以 @decorator 也可以 @decorator(args) 使用,需要更复杂的处理:

import functools

def repeat(func=None, *, times=1):
    """支持可选参数的装饰器"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper

    if func is not None:
        # 没有参数: @repeat
        return decorator(func)
    # 有参数: @repeat(times=3)
    return decorator

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

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

greet1("小红")   # 输出一次
greet2("小明")   # 输出三次

多个装饰器的堆叠顺序

多个装饰器叠加时,执行顺序是从下到上(离函数定义最近的先执行):

import functools

def deco1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("装饰器1 - 进入")
        result = func(*args, **kwargs)
        print("装饰器1 - 退出")
        return result
    return wrapper

def deco2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("装饰器2 - 进入")
        result = func(*args, **kwargs)
        print("装饰器2 - 退出")
        return result
    return wrapper

@deco1
@deco2
def say_hello(name):
    print(f"你好, {name}!")

say_hello("小明")
# 输出:
# 装饰器1 - 进入
# 装饰器2 - 进入
# 你好, 小明!
# 装饰器2 - 退出
# 装饰器1 - 退出

理解方式:say_hello = deco1(deco2(say_hello)),所以 deco2 先包装原始函数,然后 deco1 再包装。

类装饰器

装饰器不限于函数,也可以是类。类装饰器通过 __call__ 方法实现:

import functools
import time

class Timer:
    """类装饰器:统计函数执行时间"""
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func

    def __call__(self, *args, **kwargs):
        start = time.perf_counter()
        result = self.func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{self.func.__name__} 执行耗时: {elapsed:.4f}秒")
        return result

@Timer
def slow_function():
    time.sleep(0.5)
    return "完成"

print(slow_function())
# 输出:
# slow_function 执行耗时: 0.5002秒
# 完成

带参数的类装饰器

import functools

class CountCalls:
    """统计函数调用次数"""
    def __init__(self, func=None, *, verbose=False):
        functools.update_wrapper(self, func) if func else None
        self.func = func
        self.verbose = verbose
        self.count = 0

    def __call__(self, *args, **kwargs):
        if self.func is None:
            # 带参数调用时返回装饰器实例
            return type(self)(func=args[0], verbose=self.verbose)

        self.count += 1
        if self.verbose:
            print(f"{self.func.__name__} 被调用了 {self.count} 次")
        return self.func(*args, **kwargs)

@CountCalls(verbose=True)
def say_hello(name):
    print(f"你好, {name}!")

say_hello("小明")
say_hello("小红")
# 输出:
# say_hello 被调用了 1 次
# 你好, 小明!
# say_hello 被调用了 2 次
# 你好, 小红!

实战装饰器

下面介绍几个项目中常用的实用装饰器。

1. 计时器装饰器

import functools
import time

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

@timer
def compute_sum(n):
    return sum(range(n))

print(compute_sum(10_000_000))
# 输出类似: [compute_sum] 耗时: 0.234567秒

2. 日志/调试装饰器

import functools

def debug(func):
    """打印函数的调用详情"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"调用: {func.__name__}({signature})")

        result = func(*args, **kwargs)
        print(f"返回: {func.__name__}({signature}) -> {result!r}")
        return result
    return wrapper

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

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

add(3, 5)
# 调用: add(3, 5)
# 返回: add(3, 5) -> 8

greet("小明")
# 调用: greet('小明')
# 返回: greet('小明') -> '你好, 小明!'

3. 重试装饰器

import functools
import time
import random

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    """失败时自动重试的装饰器"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    print(f"第{attempt}次尝试失败: {e}{delay}秒后重试...")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unstable_network_request():
    """模拟不稳定的网络请求"""
    if random.random() < 0.7:
        raise ConnectionError("网络连接超时")
    return "请求成功!"

try:
    result = unstable_network_request()
    print(result)
except ConnectionError:
    print("所有重试都失败了")

4. 缓存/记忆化装饰器

import functools

def memoize(func):
    """缓存函数的返回值(手动实现)"""
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fibonacci(n):
    """计算斐波那契数列"""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))  # 立刻返回,不会卡死

Python 内置了更强大的缓存装饰器 @functools.lru_cache

import functools

@functools.lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(100))

# 查看缓存统计
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)

lru_cache 使用 LRU(Least Recently Used)淘汰策略,maxsize 参数控制缓存上限。

5. 限速器装饰器

import functools
import time

def rate_limit(calls_per_second):
    """限制函数每秒调用次数"""
    min_interval = 1.0 / calls_per_second
    last_called = [0.0]  # 使用列表作为可变容器

    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.perf_counter() - last_called[0]
            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)
            last_called[0] = time.perf_counter()
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(calls_per_second=2)  # 每秒最多调用2次
def api_request(url):
    print(f"请求: {url}")
    return f"数据来自 {url}"

for i in range(5):
    api_request(f"https://api.example.com/data/{i}")

6. 类型检查装饰器

import functools

def type_check(**expected_types):
    """检查函数参数类型"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 获取函数签名
            import inspect
            bound = inspect.signature(func).bind(*args, **kwargs)
            bound.apply_defaults()

            for name, value in bound.arguments.items():
                if name in expected_types:
                    expected = expected_types[name]
                    if not isinstance(value, expected):
                        raise TypeError(
                            f"参数 {name} 期望类型 {expected.__name__},"
                            f"实际得到 {type(value).__name__}"
                        )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@type_check(x=int, y=int)
def divide(x, y):
    return x / y

print(divide(10, 3))      # 正常运行
print(divide("10", 3))    # TypeError: 参数 x 期望类型 int,实际得到 str

内置装饰器回顾

Python 内置的装饰器在面向对象编程中非常常用:

@property

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        """获取半径"""
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("半径必须为正数")
        self._radius = value

    @property
    def area(self):
        """计算面积(只读属性)"""
        import math
        return math.pi * self._radius ** 2

circle = Circle(5)
print(circle.radius)   # 5(像属性一样访问)
circle.radius = 10     # 像属性一样设置
print(circle.area)     # 314.159...
# circle.area = 100    # AttributeError: can't set attribute

@staticmethod

class MathUtils:
    @staticmethod
    def is_even(n):
        """静态方法:不依赖实例或类"""
        return n % 2 == 0

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

print(MathUtils.is_even(4))   # True
print(MathUtils.add(3, 5))    # 8

@classmethod

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    @classmethod
    def from_string(cls, date_str):
        """类方法:从字符串创建实例"""
        year, month, day = map(int, date_str.split("-"))
        return cls(year, month, day)

    @classmethod
    def today(cls):
        """类方法:返回今天的日期"""
        import datetime
        now = datetime.date.today()
        return cls(now.year, now.month, now.day)

    def __repr__(self):
        return f"Date({self.year}, {self.month}, {self.day})"

d1 = Date.from_string("2026-05-21")
d2 = Date.today()
print(d1)  # Date(2026, 5, 21)
print(d2)  # Date(2026, ..., ...)

装饰器的常见陷阱

陷阱1: mutable 默认参数

def decorator_with_list(arg=[]):  # BAD: 可变默认参数只初始化一次
    ...

def decorator_with_list(arg=None):  # GOOD
    if arg is None:
        arg = []
    ...

陷阱2: 装饰器丢失原始函数信息(不用 wraps)

def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def f():
    pass

print(f.__name__)  # wrapper(错误)

陷阱3: 装饰器内的闭包变量

# BAD
def create_decorators():
    decorators = []
    for i in range(5):
        def decorator(func):
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs) + i  # i 是延迟绑定的!
            return wrapper
        decorators.append(decorator)
    return decorators

# GOOD
def create_decorators():
    decorators = []
    for i in range(5):
        def decorator(func, i=i):  # 将 i 作为默认参数,立即绑定
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs) + i
            return wrapper
        decorators.append(decorator)
    return decorators

总结与最佳实践

装饰器的应用场景

场景 说明
日志记录 自动记录函数调用和参数
性能计时 测量函数执行时间
权限校验 检查用户是否有权限执行操作
缓存 缓存函数返回结果,避免重复计算
重试 失败后自动重试
限速 控制函数调用频率
事务管理 数据库事务的开启和提交/回滚
输入验证 检查参数类型和取值范围

编写装饰器的最佳实践

  1. 始终使用 @functools.wraps 保留原函数的元信息
  2. 使用 *args, **kwargs 确保装饰器通用
  3. 保持装饰器简单——一个装饰器只做一件事
  4. 文档化你的装饰器——写清楚的文档字符串
  5. 考虑组合性——确保多个装饰器可以叠加使用
  6. 优先使用 functools 和标准库——内置工具通常更好

小结

装饰器是 Python 元编程的核心工具,它基于"函数是一等公民"的特性,允许我们以声明式的方式给函数添加额外行为。本章我们学习了装饰器的基本原理(函数嵌套函数)、@ 语法糖的工作机制、带参数装饰器的三层嵌套结构、多个装饰器的堆叠顺序,以及基于类的装饰器实现。通过计时器、日志、重试、缓存、限速和类型检查等实战示例,你应该已经掌握了装饰器的常见应用模式。最后,我们回顾了 @property@staticmethod@classmethod 这三个内置装饰器的用法。记住:始终使用 @functools.wraps,保持装饰器的单一职责,这是编写高质量装饰器的关键。

Summary: 装饰器原理、语法糖、参数传递、堆叠与实战应用。