讲解

装饰器(decorator)是「包装函数的函数」:@timer 写在函数定义上方,等价于 func = timer(func)——原函数被替换成包装后的版本,之后每次调用 func 走的都是包装层。这是 Python 高阶函数能力的集中展示:装饰器接收一个函数、返回一个函数,可以在不改动原函数源码的情况下给它加日志、计时、缓存、权限校验等「横切」能力。Flask 的路由 @app.route('/')、标准库的 @property、@functools.lru_cache 都是装饰器。

手写一个装饰器的标准模板:外层函数接收被装饰的 func,内层 wrapper(*args, **kwargs) 用收集参数接住任意调用签名,在调用 func 前后插入附加逻辑,最后 return wrapper。两个细节决定装饰器质量:一是加 @functools.wraps(func)——把原函数的 namedoc 等元信息复制到 wrapper 上,否则被装饰函数的名字、文档全部变成 wrapper 的,调试和 help() 都会错乱;二是记得 return func(*args, **kwargs) 并把结果返回,否则原函数的返回值被吞掉。

再进阶一步是「带参数的装饰器」:@repeat(3) 这种写法比无参的多包一层——repeat(3) 先返回一个装饰器,再用它装饰函数,总共三层嵌套。类也能当装饰器(实现 call),函数也能装饰类,但入门阶段掌握函数装饰函数的两层与三层模板就够用了。使用建议:装饰器会让调用栈变深、让 IDE 跳转多绕一层,确属横切关注点(与业务逻辑正交的能力)才用它,别为了炫技给每个函数都套壳。

示例

import functools
import time


# 最小装饰器:计时
def timer(func):
    @functools.wraps(func)  # 保留原函数的名字和文档
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)  # 调用原函数
        print(f'{func.__name__} 耗时 {time.perf_counter() - start:.4f} 秒')
        return result  # 别忘记把结果传出去

    return wrapper


@timer
def slow_sum(n):
    """计算 1 到 n 的和。"""
    return sum(range(n))


print('结果:', slow_sum(1000000))
print('名字保住了:', slow_sum.__name__, ',文档:', slow_sum.__doc__)


# @语法糖的本质
def shout_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper() + '!!!'

    return wrapper


def greet(name):
    return f'hello {name}'


greet = shout_decorator(greet)  # 等价于在 def 上方写 @shout_decorator
print('手动装饰:', greet('python'))


# 带参数的装饰器:三层嵌套
def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                func(*args, **kwargs)

        return wrapper

    return decorator


@repeat(3)
def say(word):
    print(word, end=' ')


say('你好')
print()


# 实战:标准库的装饰器 lru_cache 自动缓存
@functools.lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)


print('加了缓存的 fib(30):', fib(30))
print('缓存信息:', fib.cache_info())

常见坑

  • 忘写 functools.wraps:被装饰函数的 name 全变成 'wrapper',日志、调试、框架内省全乱套。写装饰器第一件事就是加它。
  • wrapper 里忘记 return:原函数的返回值被吞,调用方拿到 None。记法:接参数用 *args/**kwargs,返回值原样传出。
  • 装饰器在导入时就执行:@ 那一行在模块导入时运行,不在函数调用时。装饰器里写耗时或依赖运行时状态的逻辑会出意外。
  • 滥用装饰器藏业务逻辑:校验、转换这类与业务强相关的逻辑放进装饰器,读代码的人根本想不到去看。装饰器只装「与业务正交」的能力。

小结

@dec 等价于 func = dec(func);模板是 wrapper(*args, **kwargs) + functools.wraps + 传返回值;带参装饰器三层嵌套;lru_cache 一行加速递归。下一章快速游览标准库。