# Python 装饰器：是什么、如何工作，以及如何编写自己的装饰器

URL: https://skillaria.top/zh/guides/python/dekoratory-python

装饰器是接收一个函数并返回新函数（通常是包装器）的函数，它在调用前后添加行为，例如日志、计时、缓存或权限检查。写在 def 上方的 @decorator 等价于 func = decorator(func)；为了让包装器保留原函数的名字和文档字符串，使用 functools.wraps。

Python 中的装饰器是一个函数：它接收另一个函数，并返回一个新的函数，通常是对原函数的包装。借助它，可以在不修改函数代码的情况下为其添加日志、计时、缓存、重试等行为。装饰器写成 `@名字`，放在函数声明的上方，在 Flask、FastAPI、pytest 以及标准库中随处可见。

## 装饰器通俗地讲是什么

可以把它想象成礼物和包装纸。礼物（函数）保持不变，外面多了一层包装，它会先于礼物起作用。装饰器就是这样一层包装：调用函数时，先执行包装器的代码，包装器再调用原函数，并且可以对结果做些处理。

之所以能这样做，是因为在 Python 中函数是普通对象：可以把它们传给其他函数，从函数中返回，也可以在函数内部定义。装饰器正是建立在这三个特性之上。

## 装饰器如何工作：@ 只是语法糖

来看一个最简单的装饰器，它在调用前后打印消息：

```
def log_calls(func):
    def wrapper():
        print(f"正在调用 {func.__name__}")
        result = func()
        print(f"{func.__name__} 执行完毕")
        return result
    return wrapper

@log_calls
def hello():
    print("你好！")

hello()
```

```
正在调用 hello
你好！
hello 执行完毕
```

写在 `def` 上方的 `@log_calls` 与在声明之后写下面这行完全等价：

```
def hello():
    print("你好！")

hello = log_calls(hello)
```

现在名字 `hello` 指向的是 `wrapper`，原函数则保存在闭包中——`wrapper` 记住了外层函数中的变量 `func`。需要注意一个细节：装饰器只在 `def` 执行时应用一次，也就是模块导入时；而包装器会在每次调用时运行。

## 闭包与装饰器

闭包是一个内部函数，即使外层函数已经结束，它仍然记得外层函数的变量。没有闭包，装饰器就不可能实现：正是通过闭包，`wrapper` 才能访问 `func`。闭包中也可以保存状态，例如调用次数计数器：

```
def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: 第 {calls} 次调用")
        return func(*args, **kwargs)
    return wrapper
```

## 如何编写 Python 装饰器：通用模板

前面第一个例子有两个缺点：包装器不接收参数，而且会“吞掉”原函数的名字。下面是一个可靠的模板，编写任何装饰器都可以从它开始：

```
import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 调用之前的代码
        result = func(*args, **kwargs)
        # 调用之后的代码
        return result
    return wrapper
```

模板的三条规则：

1. **`*args, **kwargs`** — 包装器接收任意参数，并原样传递下去。
2. **`return result`** — 别忘了返回结果，否则被装饰的函数总会返回 `None`。
3. **`@functools.wraps(func)`** — 把原函数的元数据复制到包装器上。

下面是一个测量执行时间的装饰器示例：

```
import functools
import time

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:.3f} 秒")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)   # slow_sum 用时 0.142 秒
```

## 为什么需要 functools.wraps

如果不使用 `wraps`，被装饰函数的名字、文档字符串等属性都会被替换，外部看到的是 `wrapper`：

```
def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """计算两个数的和。"""
    return a + b

print(add.__name__, add.__doc__)   # wrapper None
```

使用 `@functools.wraps(func)` 之后，包装器会获得原函数的 `__name__`、`__doc__`、`__module__`、`__qualname__` 和 `__annotations__`，同时 `__wrapped__` 属性中保存着原函数的引用。这对调试和 traceback、`help()`、按名字注册函数的框架，以及显示真实签名的 `inspect.signature` 都很重要。

## 带参数的装饰器

有时装饰器需要配置，例如 `@retry(times=3)` 或 `@app.route("/")`。这时需要再增加一层嵌套：外层函数接收参数，并返回真正的装饰器——这种结构称为装饰器工厂。

```
import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def ping():
    print("ping")

ping()   # ping, ping, ping — 三行输出
```

调用顺序是：`repeat(times=3)` 返回 `decorator`，它再应用到 `ping` 上并返回 `wrapper`。换句话说，`@repeat(times=3)` 等价于 `ping = repeat(times=3)(ping)`。

一个实用的例子是出错后重试：

```
import functools
import time

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
    ...
```

## 在同一个函数上使用多个装饰器

装饰器可以叠加使用。它们从下往上应用：离 `def` 最近的先被应用。

```
@timer
@log_calls
def report():
    ...

# 等价于：
report = timer(log_calls(report))
```

调用时，最外层（`timer`）先执行，因此它也会计入 `log_calls` 的运行时间。顺序很重要：例如，一个装饰器检查访问权限，另一个缓存结果，二者的先后会影响结果。

## Python 的标准及内置装饰器

语言和标准库中已经包含许多实用的装饰器：

| 装饰器 | 作用 |
| --- | --- |

| `@property` | 把方法变成只读属性；配合 `@x.setter` 可以变成受控的可写属性 |

| `@staticmethod` | 没有 `self` 的类方法，本质上是类命名空间中的普通函数 |

| `@classmethod` | 方法接收类 `cls` 而不是实例——常用于编写替代构造函数 |

| `@functools.cache`、`@functools.lru_cache` | 按参数缓存结果 |

| `@functools.wraps` | 把函数的元数据复制到包装器上 |

| `@dataclasses.dataclass` | 为类生成 `__init__`、`__repr__`、`__eq__` |

| `@contextlib.contextmanager` | 把生成器变成可用于 `with` 的上下文管理器 |

### 缓存：functools.cache 与 lru_cache

缓存装饰器会记住每组参数对应的结果，再次调用时直接返回，而不再计算。经典例子是递归计算斐波那契数，不加缓存时耗时呈指数增长：

```
import functools

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

print(fib(100))   # 354224848179261915075
```

`functools.cache`（自 Python 3.9 起可用）保存所有结果，没有上限。`@functools.lru_cache(maxsize=128)` 只保留最近使用的若干条记录，并淘汰旧的，对长期运行的服务更安全。统计信息用 `fib.cache_info()` 查看，用 `fib.cache_clear()` 清空。这类函数的参数必须是可哈希的：传入列表会得到 `TypeError: unhashable type: 'list'`。而且只应缓存纯函数——即相同参数总是返回相同结果、没有副作用的函数。

### property 装饰器的作用

`@property` 让你可以像访问属性一样调用方法（不加括号），并在赋值时进行控制：

```
class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("余额不能为负数")
        self._balance = value

acc = Account(100)
print(acc.balance)   # 100
acc.balance = -5     # ValueError
```

### staticmethod 与 classmethod

```
class Money:
    def __init__(self, kopecks):
        self.kopecks = kopecks

    @classmethod
    def from_rubles(cls, rubles):
        return cls(round(rubles * 100))

    @staticmethod
    def is_valid(value):
        return value >= 0

m = Money.from_rubles(12.5)
print(m.kopecks, Money.is_valid(-1))   # 1250 False
```

正是 `@classmethod` 让方法可以作用于类本身，而不是实例：`from_rubles` 也能正确地创建子类的对象。

## 类装饰器与用类实现的装饰器

装饰器也可以应用于类：它接收一个类，并返回一个类（同一个或修改过的）。`@dataclass` 就是这样工作的。下面是一个注册类的简单例子：

```
registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(registry)   # {'CsvExporter': }
```

反过来，也可以用类充当装饰器。任何带有 `__call__` 方法的对象都可以，状态很适合保存在实例属性中：

```
import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def tick():
    pass

tick(); tick()
print(tick.calls)   # 2
```

## 异步装饰器

对于 `async def` 函数，包装器本身也必须是协程，并且用 `await` 等待结果，否则返回的只是一个尚未执行的协程对象：

```
import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"开始 {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"完成 {func.__name__}")
        return result
    return wrapper
```

## 装饰器有什么用

- **横切关注点** — 日志、指标、计时，无需把代码复制到每个函数中。
- **缓存** — 用 `@functools.cache` 处理昂贵的纯计算。
- **检查** — 访问权限、参数验证、Web 处理函数中的鉴权。
- **注册** — Flask 和 FastAPI 中的路由（`@app.get("/")`），pytest 中的 fixture。
- **可靠性** — 重试、超时、错误处理。

主要优点是职责分离：函数专注于自己的任务，辅助行为只需一行代码即可添加，也同样容易移除。

## 常见错误

- **包装器没有返回结果** — 被装饰的函数返回 `None`。
- **没有 `*args, **kwargs`** — 带参数调用时出现 `TypeError`。
- **忘记 `wraps`** — `__name__` 和文档字符串错误，日志难以理解。
- **工厂缺少括号** — 写成 `@repeat` 而不是 `@repeat(3)`，函数会被当作 `times` 参数传入。
- **装饰器主体中有繁重的代码** — 它在导入模块时执行，而不是在调用时执行。

## 小结

Python 装饰器是高阶函数：接收一个函数，返回包装器；`@` 只是赋值语句的简写。编写时从包含 `*args, **kwargs`、`return result` 和 `functools.wraps` 的模板开始；需要配置时，增加一层工厂函数。装饰器所依赖的函数、闭包和注解，在课程中都有实践讲解。

## FAQ

**能否取消函数上的装饰器？**

如果装饰器使用了 functools.wraps，原函数可以通过 __wrapped__ 属性访问：func.__wrapped__(...) 会直接调用未包装的函数。否则只能从代码中删除带 @ 的那一行。

**装饰器的代码什么时候执行？**

装饰器函数本身在声明被装饰的函数时执行一次，通常是在导入模块期间。wrapper 内部的代码则在每次调用被装饰的函数时执行。

**装饰器可以用于类的方法吗？**

可以。普通的 *args, **kwargs 装饰器同样适用于方法，self 只是 args 的第一个元素。

**装饰器和继承有什么区别？**

继承通过类的层次结构改变行为；装饰器从外部包装具体的函数或类，不需要修改代码，并且可以方便地与其他装饰器组合。


---
Source: Skillaria.Top (https://skillaria.top)
