函数写好了,想给它前后加点通用逻辑——记日志、算耗时、做校验——总不能每个函数都复制一遍。装饰器就是干这个的:给函数套一层"外壳",功能加上,原函数一行不改。

先看效果

import time

def timer(func):
    def wrapper():
        start = time.time()
        func()                        # 调用原来的函数
        print(f"耗时 {time.time() - start:.3f} 秒")
    return wrapper

@timer
def download():
    time.sleep(1)
    print("下载完成")

download()      # 输出:下载完成
                # 输出:耗时 1.001 秒

@timer 这一行就是装饰器,它等价于 download = timer(download)。之后再调用 download,实际跑的是 wrapper,它在原函数前后各加了一件事:掐表和报时。

原理:函数是可以传来传去的值

Python 里函数跟数字、字符串一样是个对象,可以赋值、传参、当返回值。装饰器的全部原理就一句话:接收一个函数,返回一个新函数。

def shout(fn):
    def wrapper():
        print(">>> 开嗓")
        fn()
        print("<<< 收工")
    return wrapper

def sing():
    print("唱歌~")

sing = shout(sing)   # 手动装饰,不用 @ 也能写
sing()
# 输出:>>> 开嗓
#       唱歌~
#       <<< 收工

看懂这个手动版本,@ 就只是少打一行的语法糖,没有任何魔法。

原函数有参数怎么办

被装饰的函数往往带参数,wrapper 得能接住任意参数,用 *args 和 **kwargs 通吃:

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__},参数 {args}")
        result = func(*args, **kwargs)  # 原样透传
        return result                   # 别把返回值弄丢
    return wrapper

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

print(add(3, 5))
# 输出:调用 add,参数 (3, 5)
# 输出:8

两个细节最容易踩坑:wrapper 内部要 return func(...),否则原函数的返回值会变成 None;args 原样传回,装饰器不需要知道参数具体是啥。

保留函数的"身份证"

装饰后 add.name 会变成 wrapper,调试器、文档工具都会认错人。标准库 functools 一行修好:

from functools import wraps

def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

print(add.__name__)   # 输出:add,而不是 wrapper

写自定义装饰器时建议养成肌肉记忆:@wraps(func) 永远带上,代价为零。

一个装饰器,到处复用

攒下几个常用装饰器,新函数加一行就能白得功能:

@log_call
def greet(name):
    print(f"你好,{name}")

greet("雪梨")
# 输出:调用 greet,参数 ('雪梨',)
# 输出:你好,雪梨

日志、计时、重试、缓存,这类横切的通用逻辑都适合做成装饰器,写一次到处用。

小结

  • 装饰器本质是"接收函数、返回新函数",@ 只是语法糖
  • wrapper 用 *args、**kwargs 透传参数,记得 return 保住返回值
  • functools.wraps 让装饰后的函数保留原名和文档
  • 通用逻辑(日志/计时/校验)抽成装饰器,业务函数保持干净

理解了"函数可以当值传来传去",装饰器就不再神秘。