Skip to content

Python 装饰器:给函数穿上"功能外套"

引言:咖啡加料的智慧

想象你开了一家咖啡店。菜单上有美式、拿铁、卡布奇诺……有一天,顾客提出新需求:"我想要每杯咖啡都加一份奶泡。"

你有两种选择:

  • 方案一:改造每台咖啡机——把美式机、拿铁机的内部结构全拆开,加入奶泡功能。费时费力,万一以后不加奶泡了还得改回来;
  • 方案二:做一个"奶泡机外挂"——任何咖啡做好后,从这外挂过一遍,就自动加上奶泡。咖啡机本身不用动,想加就加,想撤就撤。

Python 的**装饰器(Decorator)**就是方案二——在不修改函数本身的前提下,给函数动态增加功能。它是 Python 中最优雅、最常用的语法特性之一,Web 框架、日志系统、权限校验……到处都有它的身影。


一、热身:理解装饰器的三个前置知识

装饰器是建立在前面学过的知识之上的,先快速回顾三块"积木"。

1.1 函数也是对象,可以赋值给变量

python
def now():
    print('2024-6-1')

f = now    # 把函数赋值给变量 f(注意:没有括号!)
f()        # 通过变量调用函数
# 2024-6-1

关键点f = now 没有调用函数,只是让 f 也指向同一个函数对象。就像一个人有中文名也有英文名,叫哪个名字找到的都是同一个人。

1.2 函数有 __name__ 属性

每个函数对象都自带 __name__ 属性(前后各两个下划线),记录着函数的名字:

python
>>> now.__name__
'now'
>>> f.__name__
'now'

这个属性看起来不起眼,后面会看到它引发的一个重要问题。

1.3 高阶函数:可以接收函数、返回函数

上一篇我们学过:函数可以作为参数传入,也可以作为返回值返回。装饰器正是**"接收函数 + 返回函数"**的组合应用。


二、装饰器到底解决什么问题?

假设我们有一个函数:

python
def now():
    print('2024-6-1')

现在想给它加一个功能:每次调用时自动打印一行日志"call now():"。

最笨的办法是直接改函数:

python
def now():
    print('call now():')   # 侵入式修改
    print('2024-6-1')

这样做的麻烦:

  1. 修改了原函数,违背"尽量不动已有代码"的原则;
  2. 如果有 100 个函数都要加日志,就得改 100 次,代码里全是重复;
  3. 万一以后不想要日志了,还得再改 100 次改回来。

装饰器的思路:写一个"加日志的包装器",把 now() 塞进去,拿出来一个"带日志功能的 now()"。原函数一行不动。

生活化理解:就像给手机贴膜——手机(原函数)不用拆开改装,贴上一层膜(装饰器)就多了防刮功能,不想要了撕掉就行。


三、第一个装饰器:打印日志

3.1 完整代码

python
def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

3.2 逐行拆解

python
def log(func):                          # 第一层:接收"被装饰的函数"
    def wrapper(*args, **kw):           # 第二层:定义"包装后的新函数"
        print('call %s():' % func.__name__)   # 新增的功能:打印日志
        return func(*args, **kw)        # 调用原函数,并把结果返回
    return wrapper                      # 把新函数返回出去

三层结构各管一件事:

层级角色职责
log装饰器本体接收原函数,返回新函数
wrapper新函数先加功能,再调用原函数
func原函数被"记住"在闭包里,随时可用

wrapper 的参数写成 (*args, **kw),意思是"任何参数我都收"——这样无论原函数是无参、两个参还是带关键字参数,wrapper 都能原样转发给它。

3.3 使用装饰器:@ 语法糖

python
@log
def now():
    print('2024-6-1')

调用:

python
>>> now()
call now():
2024-6-1

一行日志自动打印,然后执行原函数。now() 的定义一行没改!

3.4 @ 背后的真相

@log 只是一个语法糖(写法上的便利),它完全等价于:

python
def now():
    print('2024-6-1')

now = log(now)   # @log 的本质就是这行代码

理解这行等价代码是理解装饰器的关键:

  1. log(now) 被调用,返回 wrapper 函数;
  2. now 这个名字重新指向 wrapper
  3. 以后调用 now(),实际执行的是 wrapper()——它先打印日志,再去调用被"记住"的原函数。

注意:原来的 now 函数并没有消失,它还活在内存里,被 wrapper 通过闭包引用着。只是 now 这个"门牌号"现在指向了新房子。

3.5 用图解梳理执行流程

定义阶段(@log 生效时):
    now ──► log(now) ──► 返回 wrapper
    now 重新指向 wrapper(wrapper 内部记住了原 now)

调用阶段(now() 时):
    now() 实际上是 wrapper()
        ├── print('call now():')     ← 新增功能
        └── return 原now()            ← 原功能
                └── print('2024-6-1')

四、进阶:带参数的装饰器

4.1 需求升级

上面的 log 只能打印固定的日志格式。如果我们想自定义日志文本,比如:

python
@log('execute')
def now():
    print('2024-6-1')

期望输出:

execute now():
2024-6-1

这时装饰器本身也要接收参数 'execute',结构就要再套一层。

4.2 三层嵌套的装饰器

python
def log(text):                          # 第一层:接收"装饰器的参数"
    def decorator(func):                # 第二层:接收"被装饰的函数"
        def wrapper(*args, **kw):       # 第三层:包装后的新函数
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

4.3 为什么需要三层?

对比两种装饰器的等价代码就明白了:

python
# 无参数装饰器(两层)
now = log(now)

# 带参数装饰器(三层)
now = log('execute')(now)

带参数的版本执行分两步:

  1. log('execute') 先执行,返回 decorator 函数(此时 'execute' 被记住);
  2. decorator(now) 再执行,返回 wrapper(此时原函数也被记住)。

生活化理解

  • 两层装饰器像"标准款贴膜"——拿来就贴;
  • 三层装饰器像"定制款贴膜"——先告诉店家你要什么图案(log('execute')),店家做好贴膜(decorator),再贴到你的手机上(decorator(now))。

4.4 记忆口诀

装饰器带参数,就多套一层函数。 最外层收参数,中间层收函数,最里层干活。


五、重要补丁:functools.wraps

5.1 一个隐藏的问题

装饰后的函数,__name__ 变了:

python
>>> @log
... def now():
...     print('2024-6-1')

>>> now.__name__
'wrapper'    # 原本是 'now',现在变成了 'wrapper'!

为什么?因为 now 现在指向的是 wrapper 函数,它的名字当然叫 'wrapper'

这有什么问题?依赖函数名和函数签名的代码会出错。比如:

  • 某些框架靠函数名自动生成文档;
  • Flask 用函数名作为 URL 路由的默认端点名;
  • 调试工具、单元测试框架会显示错误的函数名。

5.2 解决方案:@functools.wraps

Python 内置的 functools.wraps 可以把原函数的 __name____doc__ 等属性复制到 wrapper 上:

python
import functools

def log(func):
    @functools.wraps(func)          # 关键:加上这一行
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

现在:

python
>>> now.__name__
'now'    # 正常了!

带参数的装饰器同理,加在 wrapper 前面:

python
import functools

def log(text):
    def decorator(func):
        @functools.wraps(func)      # 同样加在这里
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

生活化理解:贴膜之后,手机的"身份证"(__name__)显示成了贴膜的品牌。functools.wraps 就像贴膜时把手机的身份证信息也印到膜上——外面看起来还是原来那台手机。

5.3 一条铁律

写装饰器,永远记得加 @functools.wraps(func)

即使你暂时用不到 __name__,加上它也没有任何坏处,却能避免未来莫名其妙的 bug。这是专业代码和业余代码的分水岭之一。


六、实战:常用装饰器案例

6.1 计时装饰器:测量函数执行时间

python
import time
import functools

def metric(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        start = time.time()              # 记录开始时间
        result = func(*args, **kw)       # 执行原函数
        end = time.time()                # 记录结束时间
        print('%s 执行耗时 %.2f 秒' % (func.__name__, end - start))
        return result
    return wrapper

@metric
def slow_task():
    time.sleep(1.5)
    return "任务完成"

slow_task()
# slow_task 执行耗时 1.50 秒

应用场景:性能调优时,给可疑的函数加上 @metric,立刻知道谁拖慢了程序。

6.2 权限校验装饰器

python
import functools

current_user = {"name": "小明", "is_admin": False}

def require_admin(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        if not current_user.get("is_admin"):
            print("权限不足,操作被拒绝!")
            return None
        return func(*args, **kw)
    return wrapper

@require_admin
def delete_database():
    print("数据库已删除!")

delete_database()   # 权限不足,操作被拒绝!

应用场景:Web 开发中,给"删除""修改配置"等敏感接口统一加上权限检查,不用在每个函数里重复写判断。

6.3 重试装饰器(带参数)

网络请求失败时自动重试,重试次数可配置:

python
import time
import functools

def retry(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            for attempt in range(times):
                try:
                    return func(*args, **kw)
                except Exception as e:
                    print('第 %d 次尝试失败:%s' % (attempt + 1, e))
                    if attempt < times - 1:
                        time.sleep(1)    # 等 1 秒再重试
            print('重试 %d 次后仍然失败' % times)
            return None
        return wrapper
    return decorator

@retry(3)
def fetch_data():
    import random
    if random.random() < 0.7:    # 模拟 70% 概率失败
        raise ConnectionError("网络超时")
    return "数据获取成功"

fetch_data()

这是三层装饰器的真实应用:retry(3) 先把"重试 3 次"这个配置记住,再包装函数。

6.4 缓存装饰器:避免重复计算

python
import functools

def cache(func):
    stored = {}
    @functools.wraps(func)
    def wrapper(n):
        if n not in stored:
            stored[n] = func(n)    # 没算过才算,算过直接取
        return stored[n]
    return wrapper

@cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(30))   # 没有缓存要等很久,有缓存瞬间出结果

提示:Python 内置了现成的缓存装饰器 functools.lru_cache,实际项目中直接用它即可,这里手写是为了演示原理。

6.5 一个函数叠多个装饰器

装饰器可以叠加使用,执行顺序是"从下往上包装,从上往下执行"

python
@log            # 后包装(在外面)
@metric         # 先包装(在里面)
def task():
    print("干活")

# 等价于:task = log(metric(task))

task()
# call task():        ← log 的日志先打印(外层)
# 干活
# task 执行耗时 0.00 秒  ← metric 的统计后打印(内层)

生活化理解:像穿衣服——先穿内衣(@metric),再穿外套(@log)。出门时先看到外套。


七、进阶话题

7.1 终极挑战:写一个"两种用法都支持"的装饰器

有时我们希望同一个装饰器既支持无参写法,又支持带参写法:

python
@log
def f1(): pass

@log('execute')
def f2(): pass

思路:检查第一个参数是不是函数。如果是,说明是 @log 直接装饰;否则说明是 @log('...'),先返回真正的装饰器。

python
import functools

def log(arg):
    # 情况一:arg 是函数,说明用的是 @log
    if callable(arg):
        func = arg
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('call %s():' % func.__name__)
            return func(*args, **kw)
        return wrapper
    # 情况二:arg 是文本,说明用的是 @log('...')
    else:
        text = arg
        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kw):
                print('%s %s():' % (text, func.__name__))
                return func(*args, **kw)
            return wrapper
        return decorator

@log
def f1():
    print('f1')

@log('execute')
def f2():
    print('f2')

f1()   # call f1(): 然后 f1
f2()   # execute f2(): 然后 f2

这种写法在开源库中很常见(比如 clickdjango 的某些装饰器),看懂它说明你已经真正理解了装饰器的嵌套结构。

7.2 类也可以做装饰器

除了函数,类也能实现装饰器(通过 __call__ 方法):

python
import functools

class Log:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func

    def __call__(self, *args, **kw):
        print('call %s():' % self.func.__name__)
        return self.func(*args, **kw)

@Log
def now():
    print('2024-6-1')

now()
# call now():
# 2024-6-1

什么时候用类装饰器? 当装饰器需要维护复杂状态时(比如统计调用次数、累计耗时),类天然有属性,比函数的闭包变量更清爽。

7.3 装饰器在真实世界中的应用

你日常用的框架里全是装饰器:

python
# Flask Web 框架:注册路由
@app.route('/home')
def home():
    return '欢迎回家'

# Django:要求登录
@login_required
def profile(request):
    ...

# pytest:标记跳过某个测试
@pytest.mark.skip(reason="还没修好")
def test_feature():
    ...

# Python 内置:静态方法、属性
class Circle:
    @property
    def area(self):
        return 3.14 * self.r ** 2

理解了装饰器,这些框架的"魔法"就都不再神秘。


八、常见误区与避坑指南

8.1 误区一:装饰时把函数"调用"了

python
# ❌ 错误:now() 是调用函数,传给 log 的是返回值 None
now = log(now())

# ✅ 正确:传函数本身,不加括号
now = log(now)

记忆:装饰器要的是"菜谱"(函数本身),不是"做好的菜"(执行结果)。

8.2 误区二:wrapper 里忘记 return 原函数的结果

python
# ❌ 错误:原函数的返回值被吞掉了
def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        func(*args, **kw)          # 没有 return!
    return wrapper

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

print(add(1, 2))   # call add(): 然后打印 None —— 3 不见了!

修正return func(*args, **kw),一个字母都不能少。这是个极其隐蔽的 bug——函数照常执行,但返回值悄悄变成了 None

8.3 误区三:忘记 @functools.wraps,导致函数"身份丢失"

如第五节所述,不加 functools.wraps,被装饰函数的 __name____doc__ 都会变成 wrapper 的。依赖这些属性的框架(Flask、Sphinx 文档工具等)会出问题。

铁律再强调一遍:wrapper 定义前,永远先写 @functools.wraps(func)

8.4 误区四:装饰器叠放顺序搞反

python
@require_admin   # 外层:先检查权限
@metric          # 内层:先被包装
def delete_data():
    ...

上面是先查权限再计时;如果把两个装饰器调换顺序,就变成"先开始计时,再查权限"——即使权限不足被拒绝,也会被计时统计进去。

原则:谁写在上面,谁就是"最外层包装",谁先执行。

8.5 误区五:在装饰器里做了耗时操作,拖慢导入速度

装饰器在模块导入时就会执行(@log 那一刻 log(func) 就被调用了)。如果在装饰器外层写耗时代码:

python
def log(func):
    time.sleep(10)          # ❌ 灾难:每次 import 都卡 10 秒
    def wrapper(*args, **kw):
        ...
    return wrapper

原则:装饰器外层只做"准备工作"(定义 wrapper),所有真正的逻辑都放在 wrapper 内部——它只在函数被调用时才执行。

8.6 误区六:滥用装饰器,逻辑藏得太深

装饰器虽好,但它把逻辑"藏"在了函数定义之外。如果一个函数叠了五六个装饰器,读代码的人要逐个拆开才能搞清执行顺序,反而降低可读性。

原则:装饰器用于横切的通用功能(日志、权限、计时、缓存),业务核心逻辑还是应该写在函数体内。


九、实战练习

练习 1:计时装饰器

设计一个装饰器 @metric,能作用于任何函数,并打印该函数的执行时间(毫秒)。

python
import time, functools

# 你的代码:def metric(fn): ...

@metric
def fast(x, y):
    time.sleep(0.0012)
    return x + y

@metric
def slow(x, y, z):
    time.sleep(0.1234)
    return x * y * z

f = fast(11, 22)
s = slow(11, 22, 33)
print(f, s)   # 应该输出 33 和 7986
参考答案
python
import time, functools

def metric(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kw):
        start = time.time()
        result = fn(*args, **kw)
        end = time.time()
        print('%s 执行了 %.2f ms' % (fn.__name__, (end - start) * 1000))
        return result
    return wrapper

易错点:别忘了 return result,否则函数返回值会丢失。

练习 2:前后日志

编写一个装饰器,在函数调用前后分别打印 'begin call''end call'

python
@before_after
def task():
    print("执行中...")

# 期望输出:
# begin call
# 执行中...
# end call
参考答案
python
import functools

def before_after(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('begin call')
        result = func(*args, **kw)
        print('end call')
        return result
    return wrapper

练习 3:调用次数统计

写一个装饰器 @count_calls,统计函数被调用了多少次,每次调用时打印"第 N 次调用"。

参考答案
python
import functools

def count_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        wrapper.count += 1          # 用函数属性存计数
        print('第 %d 次调用 %s' % (wrapper.count, func.__name__))
        return func(*args, **kw)
    wrapper.count = 0               # 初始化计数器
    return wrapper

@count_calls
def hello():
    print("你好")

hello()   # 第 1 次调用 hello
hello()   # 第 2 次调用 hello

技巧:函数也是对象,可以挂属性(wrapper.count),适合存这种轻量状态。状态复杂时建议改用类装饰器。


十、小结

  1. 装饰器的本质:一个"接收函数、返回函数"的高阶函数,在不修改原函数代码的前提下动态增强功能;
  2. @ 语法糖@log 等价于 now = log(now),函数名重新指向包装后的新函数;
  3. 带参数的装饰器要三层嵌套:最外层收参数,中间层收函数,最里层干活;
  4. 永远加 @functools.wraps(func):保留原函数的 __name__ 等元信息,避免框架和调试工具出问题;
  5. wrapper 里记得 return func(\*args, **kw):漏掉 return 会悄悄吞掉原函数的返回值;
  6. 多个装饰器叠加:从下往上包装,从上往下执行;
  7. 装饰器适合横切关注点:日志、计时、权限、缓存、重试……这些"每个函数都要,但和业务无关"的功能。

装饰器是 Python 函数式编程的集大成者——它用到了函数即对象、高阶函数、闭包等几乎所有前置知识。掌握了装饰器,你就拿到了阅读 Flask、Django 等框架源码的钥匙,也拥有了用几行代码给整个项目"加buff"的能力。

上一篇:匿名函数 lambda