前言:为什么需要装饰器
在Python开发的日常工作中,我们经常需要在已有函数的基础上添加额外功能——比如记录日志、统计执行时间、权限校验、缓存结果等。最直接的想法是修改函数本身的代码,但这违反了开闭原则(对扩展开放、对修改关闭)。装饰器(Decorator)正是Python为解决这个问题而提供的优雅语法糖。
本质上,装饰器就是一个接受函数作为参数并返回新函数的高阶函数。它让我们在不修改原函数代码的前提下,为其动态添加行为。这个看似简单的概念,却是Python元编程的基石之一,在Flask、Django、FastAPI等主流框架中无处不在。
本文将从最基础的函数对象开始,循序渐进地覆盖装饰器的全部核心用法,包括带参装饰器、类装饰器、functools.wraps的使用、装饰器链式组合,以及生产环境中的实战案例。
一、Python函数是一等公民
理解装饰器之前,必须先确认一个关键认知:Python中的函数是一等对象(first-class object)。这意味着函数可以赋值给变量、作为参数传递、从其他函数中返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 # 函数可以赋值给变量
def greet(name):
return f"Hello, {name}!"
say_hello = greet
print(say_hello("Alice")) # 输出: Hello, Alice!
# 函数可以作为参数传递
def call_twice(func, arg):
return func(func(arg))
print(call_twice(greet, "Bob")) # 输出: Hello, Hello, Bob!!
# 函数可以嵌套定义并返回
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10))
print(triple(10))
上面的例子中,
1 | make_multiplier |
返回了内部定义的
1 | multiplier |
函数,并且
1 | multiplier |
捕获了外层作用域的变量
1 | n |
——这就是闭包(Closure)的基本形态。装饰器的核心机制,正是建立在函数的这种一等公民特性和闭包之上的。
二、从零实现一个装饰器
2.1 最简单的装饰器
假设我们需要统计每个函数的执行耗时。最朴素的做法是在每个函数里手动加计时代码,但这非常冗余。装饰器可以一次性解决:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import time
def timer(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
def slow_add(a, b):
time.sleep(0.5)
return a + b
slow_add = timer(slow_add)
print(slow_add(3, 5))
2.2 @语法糖
上面的写法虽然功能正确,但每次都要写一遍
1 | func = decorator(func) |
很烦人。Python提供了
1 | @ |
语法糖让装饰更加直观:
1
2
3
4 @timer
def slow_add(a, b):
time.sleep(0.5)
return a + b
使用
1 | @timer |
放在函数定义上方,Python会在函数定义完成后自动执行
1 | timer(slow_add) |
并将结果赋给
1 | slow_add |
。这就是装饰器的完整工作流程。
三、functools.wraps——不要丢失元信息
上面的装饰器有一个隐藏的缺陷:被装饰后的函数丢掉了原来的元数据。让我们来看看问题:
1
2
3
4
5
6
7
8 def slow_add(a, b):
"""计算两个数的和,模拟耗时操作"""
time.sleep(0.5)
return a + b
decorated = timer(slow_add)
print(decorated.__name__) # 'wrapper'
print(decorated.__doc__) # None
这是因为我们返回的是
1 | wrapper |
函数,原函数的
1 | __name__ |
和
1 | __doc__ |
都丢失了。标准的修复方案是使用
1 | functools.wraps |
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import functools
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:.4f}s")
return result
return wrapper
@timer
def slow_add(a, b):
"""计算两个数的和,模拟耗时操作"""
time.sleep(0.5)
return a + b
print(slow_add.__name__) # 'slow_add'
print(slow_add.__doc__)
1 | @functools.wraps |
是装饰器之上的装饰器(meta-decorator),它在内部做了三件事:将
1 | func.__name__ |
、
1 | func.__doc__ |
、
1 | func.__module__ |
等属性复制到
1 | wrapper |
上,同时更新
1 | wrapper.__wrapped__ |
指向原始函数。这是一个所有自定义装饰器都应该使用的标准做法。
四、带参数的装饰器
上一节的计时器只能输出固定的消息格式。如果希望装饰器本身接受参数(比如日志级别、重试次数),就需要再包一层:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import functools
def repeat(times=3):
"""带参数的装饰器:让函数重复执行指定次数"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
result = func(*args, **kwargs)
if i < times - 1:
print(f"第 {i+1} 次执行完毕,继续...")
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("World")
注意这里有三层嵌套:最外层
1 | repeat(times=3) |
接收装饰器参数并返回真正的装饰器
1 | decorator |
,中间层接收函数,内层是实际的
1 | wrapper |
。
4.1 参数可选支持
某些场景下我们希望
1 | @timer |
和
1 | @timer(unit='ms') |
两种写法都支持。实现方式是利用参数检测:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 def timer(func=None, unit='s'):
if func is None:
return lambda f: timer(f, unit=unit)
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
if unit == 'ms':
print(f"[{func.__name__}] 耗时: {elapsed*1000:.2f}ms")
else:
print(f"[{func.__name__}] 耗时: {elapsed:.4f}s")
return result
return wrapper
@timer
def foo(): time.sleep(0.1)
@timer(unit='ms')
def bar(): time.sleep(0.1)
五、类装饰器
装饰器不一定要用函数实现,用类也可以。只要类实现了
1 | __call__ |
方法,它的实例就可以像函数一样被调用:
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 import functools
import time
class Timer:
"""类实现的计时装饰器"""
def __init__(self, func=None, unit='s'):
self.func = func
self.unit = unit
if func is not None:
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
start = time.perf_counter()
result = self.func(*args, **kwargs)
elapsed = time.perf_counter() - start
if self.unit == 'ms':
print(f"[{self.func.__name__}] 耗时: {elapsed*1000:.2f}ms")
else:
print(f"[{self.func.__name__}] 耗时: {elapsed:.4f}s")
return result
@Timer
def compute():
time.sleep(0.2)
return 42
类装饰器的一个优势是可以保存状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 class CountCalls:
"""统计函数调用次数的类装饰器"""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"调用 #{self.count} 次")
return self.func(*args, **kwargs)
@CountCalls
def say(message):
print(f"说: {message}")
六、多个装饰器的组合顺序
实际项目中经常需要叠加多个装饰器。装饰器的叠加顺序从下往上应用,从上往下执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 def bold(func):
@functools.wraps(func)
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def hello():
return "Hello World"
print(hello()) # <b><i>Hello World</i></b>
| 叠加方式 | 等价代码 | 执行顺序 | ||||
|---|---|---|---|---|---|---|
|
|
最外层A先执行,再B,最后原始函数 | ||||
|
|
先计算B(arg),再叠加A |
七、实战案例:生产级装饰器
7.1 重试装饰器(指数退避)
网络请求常常因临时故障失败,重试机制是必备的。以下装饰器实现了指数退避(Exponential Backoff)重试策略:
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 time
import random
def retry(max_retries=3, base_delay=1, backoff_factor=2, exceptions=(Exception,)):
"""带指数退避的重试装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_retries:
delay = base_delay * (backoff_factor ** attempt) + random.uniform(0, 0.1)
print(f"[重试 {attempt+1}/{max_retries}] 失败: {e}, {delay:.2f}s后重试...")
time.sleep(delay)
raise last_exception
return wrapper
return decorator
@retry(max_retries=3, base_delay=0.5, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url):
if random.random() < 0.7:
raise ConnectionError(f"连接 {url} 超时")
return f"来自 {url} 的数据"
7.2 缓存装饰器(TTL支持)
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 import functools
import time
from collections import OrderedDict
def ttl_cache(maxsize=128, ttl=60):
def decorator(func):
cache = OrderedDict()
cache_times = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(sorted(kwargs.items()))
now = time.time()
if key in cache:
if now - cache_times[key] < ttl or ttl <= 0:
cache.move_to_end(key)
return cache[key]
else:
del cache[key]; del cache_times[key]
result = func(*args, **kwargs)
if len(cache) >= maxsize:
oldest = next(iter(cache))
del cache[oldest]; del cache_times[oldest]
cache[key] = result
cache_times[key] = now
return result
wrapper.cache_info = lambda: {'size': len(cache), 'maxsize': maxsize, 'ttl': ttl}
wrapper.cache_clear = cache.clear
return wrapper
return decorator
@ttl_cache(maxsize=32, ttl=30)
def expensive_computation(n):
time.sleep(2)
return n * n
7.3 权限校验装饰器
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 import functools
class User:
def __init__(self, name, roles):
self.name = name
self.roles = roles
def require_role(required_roles):
if isinstance(required_roles, str):
required_roles = [required_roles]
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
user = kwargs.get('user')
if user is None and args:
user = args[0]
if user is None:
raise PermissionError("缺少用户信息")
if not any(role in user.roles for role in required_roles):
raise PermissionError(f"用户 {user.name} 需要角色 {required_roles}")
return func(*args, **kwargs)
return wrapper
return decorator
@require_role(['admin', 'editor'])
def delete_post(post_id, user=None):
return f"用户 {user.name} 已删除文章 #{post_id}"
7.4 单例模式装饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import functools
def singleton(cls):
instances = {}
@functools.wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
self.connected = False
print("创建数据库连接...")
db1 = Database()
db2 = Database()
print(db1 is db2) # True
八、装饰器的高级话题
8.1 @dataclass 原理
Python 3.7引入的
1 | @dataclass |
可能是最著名的内置装饰器之一。它自动为类生成
1 | __init__ |
、
1 | __repr__ |
、
1 | __eq__ |
等方法:
1
2
3
4
5
6
7
8
9
10 from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p) # Point(x=1.0, y=2.0)
print(p == Point(1.0, 2.0)) # True
8.2 装饰器与描述符协议
当装饰器应用于类的方法时,如果想让装饰器实例成为描述符(descriptor),需要实现
1 | __get__ |
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13 class BoundMethodDecorator:
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
if obj is None:
return self
import functools
@functools.wraps(self.func)
def bound(*args, **kwargs):
return self.func(obj, *args, **kwargs)
return bound
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
九、总结与最佳实践
装饰器是Python中极具表现力的特性,合理使用可以让代码更加简洁、可维护。以下是几条核心建议:
- 永远使用
:这是写出正确装饰器的第一原则,否则被装饰函数会丢失元信息,导致调试困难和文档工具失效。1@functools.wraps
- 一层做一件事:如果一个装饰器承担了多个职责,拆分成多个独立的装饰器叠加使用,更符合单一职责原则。
- 注意性能开销:装饰器在每次函数调用时都会执行额外逻辑。对于高频调用的函数,要考虑装饰器本身的开销。
- 优先使用带参数的装饰器:灵活的参数设计可以覆盖更多使用场景,且通过参数检测技巧可以同时支持带参和无参两种调用方式。
- 与上下文管理器结合:某些场景下(比如数据库事务)使用
1with
语句比装饰器更合适。装饰器适合横切关注点,上下文管理器适合资源管理。
- 了解内置装饰器:
1@staticmethod
、
1@classmethod、
1@property、
1@functools.lru_cache、
1@dataclass等内置装饰器足以应对大多数常见需求。
装饰器的本质是函数式编程中高阶函数思想在Python中的具体实现。掌握了装饰器,你不仅获得了一项实用的编码技巧,更深入理解了Python运行时的函数调用模型和元编程能力。在生产项目中合理使用装饰器,可以大幅减少样板代码,让核心业务逻辑更加清晰突出。


汤不热吧