装饰器入门

装饰器(decorator)是 Python 里”看起来很魔法,其实很简单”的语法。你在 Flask、Django、FastAPI、pytest 的代码里到处都能看到 @ 符号,那就是装饰器。前面 作用域与闭包匿名函数 lambda 与高阶函数 讲的东西合体,就是装饰器。这一篇讲清楚装饰器的原理、语法糖 @functools.wraps 的作用,进阶部分(带参数、类装饰器、lru_cache)留到 装饰器进阶

一、装饰器是什么

一句话定义:装饰器是一个”接受函数、返回函数”的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def uppercase(func):
def wrapper():
result = func()
return result.upper()
return wrapper


def greet():
return "hello"


greet = uppercase(greet) # 手动装饰
print(greet()) # HELLO

语义:”用 uppercase 包装一下 greet,让它返回值自动大写。”

二、@ 语法糖

@decorator 就是”把下面这个函数传给 decorator,再把返回值绑回同名”:

1
2
3
4
5
6
7
8
@uppercase
def greet():
return "hello"

# 等价于
def greet():
return "hello"
greet = uppercase(greet)

@ 只是让代码更短、意图更明显,本质没差。

三、带参数的被装饰函数

真实场景函数都有参数,wrapper 用 *args**kwargs 通吃:

1
2
3
4
5
6
7
8
9
10
11
12
13
def uppercase(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper() if isinstance(result, str) else result
return wrapper


@uppercase
def greet(name):
return f"hello {name}"


print(greet("咖飞")) # HELLO 咖飞

四、functools.wraps:保留原函数元信息

被装饰后,函数的名字和 docstring 会被替换:

1
2
3
4
5
6
7
@uppercase
def greet(name):
"""给某人打招呼"""
return f"hello {name}"

print(greet.__name__) # 'wrapper' ← 不是 greet 了!
print(greet.__doc__) # None

functools.wraps 修复这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import functools

def uppercase(func):
@functools.wraps(func) # ← 加这一行
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper() if isinstance(result, str) else result
return wrapper

@uppercase
def greet(name):
"""给某人打招呼"""
return f"hello {name}"

print(greet.__name__) # 'greet'
print(greet.__doc__) # '给某人打招呼'

规则:只要写装饰器,就无脑加 @functools.wraps(func)

五、经典装饰器:计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
import functools


def timed(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:.4f}s")
return result
return wrapper


@timed
def compute(n):
return sum(i * i for i in range(n))


compute(1_000_000)

六、经典装饰器:日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import functools
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger(__name__)


def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
log.info(f"调用 {func.__name__}(args={args}, kwargs={kwargs})")
try:
result = func(*args, **kwargs)
except Exception as e:
log.exception(f"{func.__name__} 抛出 {e}")
raise
log.info(f"{func.__name__} 返回 {result!r}")
return result
return wrapper


@logged
def divide(a, b):
return a / b


divide(10, 2)
divide(10, 0) # 会抛异常,日志里会记录

七、经典装饰器:重试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import functools
import time


def retry(times=3, delay=1):
"""遇到异常重试。"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exc = e
print(f"[{attempt}] {func.__name__} 失败:{e}")
if attempt < times:
time.sleep(delay)
raise last_exc
return wrapper
return decorator


@retry(times=3, delay=1)
def flaky_api():
import random
if random.random() < 0.7:
raise RuntimeError("network error")
return "ok"


print(flaky_api())

这已经是”带参数的装饰器”——外层 retry(times, delay) 返回真正的装饰器。三层嵌套是这个模式的标配。带参数装饰器我们在 装饰器进阶 深入讲。

八、内置装饰器

Python 有几个常用装饰器:

@staticmethod / @classmethod / @property

在类里用,我们在 类方法、静态方法与 property 讲。

@functools.lru_cache / @functools.cache(Python 3.9+)

自动缓存函数结果:

1
2
3
4
5
6
7
8
9
10
11
from functools import lru_cache, cache

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

# Python 3.9+ 无界版
@cache
def fib(n): ...

注意lru_cache 要求参数可哈希(不能是 list/dict)。

@functools.total_ordering

给类补齐比较方法:定义 __eq__ 和一个顺序方法(如 __lt__),自动生成剩下 3 个。见 特殊方法

@dataclass

自动生成 __init____repr__ 等,见 类与对象基础

九、多层装饰器

装饰器可以叠加,从下往上执行:

1
2
3
4
5
6
@a
@b
@c
def f(): ...

# 等价:f = a(b(c(f)))

调用 f() 时,实际是 a 的 wrapper 最外层调用。想象成”洋葱”——最外层先接触,逐层剥入到 f。

十、装饰类的方法

装饰器可以用在类的方法上,但要注意 self

1
2
3
4
5
6
7
8
9
10
11
12
13
def logged(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs): # self 也在 *args 里
print(f"[LOG] {self.__class__.__name__}.{func.__name__}")
return func(self, *args, **kwargs)
return wrapper

class Service:
@logged
def run(self, task):
print(f"运行 {task}")

Service().run("build")

def wrapper(*args, **kwargs) 也行,self 就是 args[0]

十一、装饰器的调试小技巧

被装饰后调试有时候会迷惑:

1
2
3
4
5
@timed
def foo(): ...

foo.__wrapped__ # 就是原来的 foo(wraps 提供的)
foo.__name__ # 'foo'

如果你想临时”取消装饰”,可以 foo.__wrapped__() 调用原函数。

十二、装饰器的常见错误

错误 1:忘了写 wrapper

1
2
def bad(func):
return func() # ❌ 立刻调用了!应该 return func 或 return wrapper

装饰器必须返回一个函数,不是”函数的返回值”。

错误 2:wrapper 没接收参数

1
2
3
4
5
6
7
8
9
def bad(func):
def wrapper(): # ❌ 没有 *args, **kwargs
return func()
return wrapper

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

add(1, 2) # TypeError: wrapper() takes 0 positional

错误 3:忘了 return

1
2
3
4
5
def bad(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs) # ❌ 忘了 return
return wrapper

被装饰的函数返回值丢了。

错误 4:忘了 @functools.wraps

被装饰后 func.__name__func.__doc__ 都乱了,pytest / Sphinx / 日志都会受影响。

十三、一个完整例子:权限校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# permission.py
"""装饰器实现的简单权限校验。"""

import functools


class Unauthorized(Exception):
pass


current_user = {"name": "guest", "roles": {"user"}}


def require_role(role: str):
"""要求当前用户拥有指定 role。"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if role not in current_user["roles"]:
raise Unauthorized(
f"{current_user['name']} 缺少 {role} 权限")
return func(*args, **kwargs)
return wrapper
return decorator


@require_role("admin")
def delete_user(id: int):
print(f"删除用户 {id}")


@require_role("user")
def read_profile():
print("查看资料")


read_profile() # ok
try:
delete_user(42) # 缺 admin,抛异常
except Unauthorized as e:
print(e)

# 升权限
current_user["roles"].add("admin")
delete_user(42) # ok

Flask 的 @login_required、Django 的 @permission_required 都是这个思路的产业级实现。

十四、装饰器 vs 上下文管理器 vs 中间件

三者常做类似的事情(”包一层”):

  • 装饰器:包”函数”;
  • 上下文管理器(with):包”代码块”;
  • 中间件:包”请求-响应流”。

选择规则:

  • 想给一个函数加”计时/日志/权限/重试” → 装饰器;
  • 想给一段代码加”资源打开/关闭” → 上下文管理器;
  • 想在框架级给每个请求加逻辑 → 中间件。

十五、小结与延伸阅读

  • 装饰器就是”接受函数、返回函数”的函数;
  • @decorator 只是 func = decorator(func) 的语法糖;
  • wrapper 用 *args, **kwargs 通吃参数;
  • 一定要 @functools.wraps(func) 保留元信息;
  • 常见装饰器:timed、logged、retry、cached、permission;
  • 装饰器可以叠加,从下往上;
  • 带参数装饰器需要三层嵌套。

延伸阅读:

下一篇 模块与包管理 我们要跳出单文件,学会把代码组织成模块和包。