欢迎光临
我们一直在努力

Python装饰器(Decorator)从入门到精通:原理、实战与高级用法

前言:为什么需要装饰器

在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>
叠加方式 等价代码 执行顺序
1
@A @B def f():
1
f = A(B(f))
最外层A先执行,再B,最后原始函数
1
@A @B(arg)
1
f = A(B(arg)(f))
先计算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

    :这是写出正确装饰器的第一原则,否则被装饰函数会丢失元信息,导致调试困难和文档工具失效。

  • 一层做一件事:如果一个装饰器承担了多个职责,拆分成多个独立的装饰器叠加使用,更符合单一职责原则。
  • 注意性能开销:装饰器在每次函数调用时都会执行额外逻辑。对于高频调用的函数,要考虑装饰器本身的开销。
  • 优先使用带参数的装饰器:灵活的参数设计可以覆盖更多使用场景,且通过参数检测技巧可以同时支持带参和无参两种调用方式。
  • 与上下文管理器结合:某些场景下(比如数据库事务)使用
    1
    with

    语句比装饰器更合适。装饰器适合横切关注点,上下文管理器适合资源管理。

  • 了解内置装饰器
    1
    @staticmethod

    1
    @classmethod

    1
    @property

    1
    @functools.lru_cache

    1
    @dataclass

    等内置装饰器足以应对大多数常见需求。

装饰器的本质是函数式编程中高阶函数思想在Python中的具体实现。掌握了装饰器,你不仅获得了一项实用的编码技巧,更深入理解了Python运行时的函数调用模型和元编程能力。在生产项目中合理使用装饰器,可以大幅减少样板代码,让核心业务逻辑更加清晰突出。

Python代码编辑器截图

Python装饰器概念图

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Python装饰器(Decorator)从入门到精通:原理、实战与高级用法
分享到: 更多 (0)