跳到主要内容

Python 装饰器

装饰器是 Python 中最优雅、最强大的特性之一。它允许你在不修改原函数代码的情况下,增强或改变函数的行为。这种"切面编程"的思想,让代码更加简洁、可维护,也更容易复用。

装饰器是什么?

从本质上说,装饰器是一个函数,它接收一个函数作为参数,并返回一个新的函数。这个新函数通常会在调用原函数之前或之后,执行一些额外的操作。

根据 Python 官方 PEP 318 的定义,装饰器的设计目标是将函数的"变换"操作放在函数声明处,而不是函数定义之后。这解决了传统写法中"函数名重复出现三次"的问题:

# 传统写法(Python 2.2-2.3)
def foo(cls):
perform_method_operation
foo = classmethod(foo) # 函数名出现了三次!

# 装饰器语法(Python 2.4+)
@classmethod
def foo(cls):
perform_method_operation

装饰器的名字来源于编译器领域——在语法树遍历过程中对节点进行"标注"(decorate)。虽然这个名字与 GoF 设计模式中的"装饰器模式"不完全一致,但核心思想是相通的:动态地为对象添加额外的职责。

理解函数:装饰器的基础

要真正理解装饰器,必须先理解 Python 中函数的几个关键特性。

函数是一等公民

在 Python 中,函数和整数、字符串一样,都是对象。这意味着函数可以:

def say_hello():
return "Hello"

def say_goodbye():
return "Goodbye"

# 1. 函数可以赋值给变量
func = say_hello
print(func()) # Hello

# 2. 函数可以作为参数传递
def greet(func):
"""接收一个函数作为参数并调用它"""
print(func())

greet(say_hello) # Hello
greet(say_goodbye) # Goodbye

# 3. 函数可以作为返回值
def get_greeting(type):
"""根据类型返回不同的函数"""
if type == "morning":
return say_hello
else:
return say_goodbye

morning_greeting = get_greeting("morning")
print(morning_greeting()) # Hello

关键区分say_hello 是函数对象的引用,而 say_hello() 是调用函数并获取返回值。理解这一点对于掌握装饰器至关重要。

内部函数(嵌套函数)

Python 允许在函数内部定义另一个函数:

def parent():
print("进入 parent()")

def first_child():
print("我是 first_child")

def second_child():
print("我是 second_child")

# 调用内部函数
second_child()
first_child()

print("离开 parent()")

parent()
# 输出:
# 进入 parent()
# 我是 second_child
# 我是 first_child
# 离开 parent()

重要特性

  • 内部函数在父函数被调用时才定义
  • 内部函数是父函数的局部变量,外部无法直接访问
  • 内部函数可以访问父函数的局部变量(闭包)

闭包:记住外部变量

闭包是指内部函数"记住"了它被定义时的环境:

def make_multiplier(factor):
"""创建一个乘法器"""
def multiplier(number):
return number * factor # factor 来自外部函数
return multiplier

# 创建不同的乘法器
double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5)) # 10 (5 * 2)
print(triple(5)) # 15 (5 * 3)

multiplier 函数"闭包"了 factor 变量。即使 make_multiplier 已经执行完毕,multiplier 仍然能访问 factor。这正是装饰器能够"记住"被装饰函数的原因。

基本装饰器

从零开始理解装饰器

让我们从最简单的例子开始,一步步理解装饰器的工作原理:

def my_decorator(func):
"""一个简单的装饰器"""
def wrapper():
print("调用函数之前")
func() # 调用原始函数
print("调用函数之后")
return wrapper

def say_hello():
print("Hello!")

# 手动应用装饰器
say_hello = my_decorator(say_hello)
say_hello()
# 输出:
# 调用函数之前
# Hello!
# 调用函数之后

执行过程分析

  1. my_decorator(say_hello) 被调用,传入原始的 say_hello 函数
  2. my_decorator 内部定义了 wrapper 函数
  3. wrapper 函数内部引用了 func(通过闭包)
  4. my_decorator 返回 wrapper 函数
  5. say_hello 现在指向 wrapper 函数
  6. 调用 say_hello() 实际上是调用 wrapper()

使用 @ 语法糖

Python 2.4 引入了 @ 语法,让装饰器的使用更加简洁:

def my_decorator(func):
def wrapper():
print("调用函数之前")
func()
print("调用函数之后")
return wrapper

@my_decorator # 等价于 say_hello = my_decorator(say_hello)
def say_hello():
print("Hello!")

say_hello()

@decorator 语法只是一种便利写法,它在函数定义后立即执行 func = decorator(func)。这个语法被称为"pie syntax"(派语法),因为 @ 看起来像一个派。

装饰器的执行时机

一个重要但容易被忽视的事实:装饰器在函数定义时执行,而不是在函数调用时执行

def my_decorator(func):
print(f"装饰器执行:正在装饰 {func.__name__}")
def wrapper():
print("wrapper 被调用")
func()
return wrapper

@my_decorator # 这里就会打印 "装饰器执行..."
def say_hello():
print("Hello!")

# 此时还没有调用 say_hello()

say_hello() # 现在才打印 wrapper 相关内容

这意味着装饰器可以在模块加载时进行初始化工作,比如注册函数、验证配置等。

处理参数和返回值

通用装饰器模板

最实用的装饰器应该能够处理任意参数并正确传递返回值:

import functools

def decorator(func):
"""通用装饰器模板"""
@functools.wraps(func) # 保留原函数的元信息
def wrapper(*args, **kwargs):
# 调用前的操作
result = func(*args, **kwargs) # 调用原函数
# 调用后的操作
return result
return wrapper

为什么需要 *args**kwargs

如果装饰器只适用于无参函数,它的用途会非常有限:

# 错误示例:只能装饰无参函数
def bad_decorator(func):
def wrapper():
return func() # 无法传递参数
return wrapper

@bad_decorator
def greet(name):
print(f"Hello, {name}")

greet("Alice") # TypeError: wrapper() takes 0 positional arguments but 1 was given

正确的做法是使用 *args**kwargs 接收任意参数:

import functools

def do_twice(func):
"""执行函数两次"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs) # 第一次执行
return func(*args, **kwargs) # 第二次执行并返回结果
return wrapper

@do_twice
def greet(name):
print(f"Hello, {name}")
return f"Greeting for {name}"

result = greet("Alice")
# 输出:
# Hello, Alice
# Hello, Alice

print(result) # Greeting for Alice

使用 functools.wraps 保留元信息

被装饰后的函数会"丢失"原有的名称和文档字符串:

def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@my_decorator
def example():
"""这是一个示例函数"""
pass

print(example.__name__) # wrapper(不是 example!)
print(example.__doc__) # None(文档丢失了)

使用 @functools.wraps 可以解决这个问题:

import functools

def my_decorator(func):
@functools.wraps(func) # 复制原函数的元信息
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@my_decorator
def example():
"""这是一个示例函数"""
pass

print(example.__name__) # example
print(example.__doc__) # 这是一个示例函数

functools.wraps 实际上调用了 functools.update_wrapper(),它会复制以下属性:

  • __name__:函数名称
  • __doc__:文档字符串
  • __module__:所属模块
  • __annotations__:类型注解
  • __qualname__:限定名称
  • __wrapped__:指向原函数的引用(Python 3.2+)

最佳实践:始终使用 @functools.wraps 装饰器,除非有特殊理由不这样做。

带参数的装饰器

有时我们需要让装饰器接受参数,比如指定重试次数、设置超时时间等。

带参数装饰器的结构

带参数的装饰器实际上是"装饰器工厂"——它返回一个装饰器:

import functools

def repeat(times):
"""装饰器工厂:返回一个装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(times=3)
def say_hello():
print("Hello!")

say_hello()
# 输出:
# Hello!
# Hello!
# Hello!

执行过程

  1. @repeat(times=3) 先调用 repeat(3),返回 decorator 函数
  2. 然后执行 say_hello = decorator(say_hello)
  3. 最终 say_hello 指向 wrapper 函数

两种调用方式对比

# 方式一:使用 @ 语法
@repeat(times=3)
def greet(name):
print(f"Hello, {name}")

# 方式二:手动调用(等价于方式一)
def greet(name):
print(f"Hello, {name}")
greet = repeat(times=3)(greet)

可选参数的装饰器

有时我们需要装饰器既能接受参数,也能不接受参数:

import functools

def repeat(func=None, *, times=2):
"""可接受参数也可不接受的装饰器"""
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
for _ in range(times):
result = f(*args, **kwargs)
return result
return wrapper

if func is None:
# 被调用时带有参数:@repeat(times=3)
return decorator
else:
# 被调用时不带参数:@repeat
return decorator(func)

# 不带参数
@repeat
def say_hello():
print("Hello!")

# 带参数
@repeat(times=3)
def greet(name):
print(f"Hi, {name}")

实用装饰器示例

1. 计时装饰器

测量函数执行时间,用于性能分析:

import functools
import time

def timer(func):
"""测量函数执行时间"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter() # 高精度计时
result = func(*args, **kwargs)
end_time = time.perf_counter()
elapsed = end_time - start_time
print(f"[{func.__name__}] 执行耗时: {elapsed:.4f} 秒")
return result
return wrapper

@timer
def slow_function():
time.sleep(1)
return "完成"

slow_function() # [slow_function] 执行耗时: 1.0012 秒

2. 日志装饰器

记录函数调用信息,用于调试:

import functools
import logging

logging.basicConfig(level=logging.INFO)

def log(func):
"""记录函数调用日志"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 构建参数字符串
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={repr(v)}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)

logging.info(f"调用 {func.__name__}({signature})")
result = func(*args, **kwargs)
logging.info(f"{func.__name__} 返回: {repr(result)}")

return result
return wrapper

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

add(3, 5)
# INFO:root:调用 add(3, 5)
# INFO:root:add 返回: 8

3. 缓存装饰器

缓存函数结果,避免重复计算:

import functools

def memoize(func):
"""缓存函数结果"""
cache = {}

@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]

# 提供清除缓存的方法
wrapper.cache_clear = cache.clear
wrapper.cache_info = lambda: f"缓存大小: {len(cache)}"

return wrapper

@memoize
def fibonacci(n):
"""计算斐波那契数列"""
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(100)) # 瞬间完成,因为结果被缓存
print(fibonacci.cache_info()) # 缓存大小: 101

Python 标准库提供了更强大的 @functools.lru_cache

from functools import lru_cache

@lru_cache(maxsize=128) # 最多缓存 128 个结果
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

4. 重试装饰器

自动重试失败的函数调用:

import functools
import time

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
"""自动重试装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts:
print(f"第 {attempt} 次尝试失败: {e}{delay}秒后重试...")
time.sleep(delay)
raise last_exception
return wrapper
return decorator

@retry(max_attempts=3, delay=1, exceptions=(ValueError,))
def unstable_function():
import random
if random.random() < 0.7:
raise ValueError("随机失败")
return "成功"

result = unstable_function()

5. 类型检查装饰器

验证函数参数和返回值的类型:

import functools

def validate_types(**types):
"""类型验证装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 获取参数绑定
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()

# 验证参数类型
for name, value in bound.arguments.items():
if name in types and not isinstance(value, types[name]):
raise TypeError(
f"参数 '{name}' 应该是 {types[name]},"
f"实际是 {type(value)}"
)

result = func(*args, **kwargs)

# 验证返回值类型
if 'return' in types and not isinstance(result, types['return']):
raise TypeError(
f"返回值应该是 {types['return']},"
f"实际是 {type(result)}"
)

return result
return wrapper
return decorator

@validate_types(a=int, b=int, return=int)
def add(a, b):
return a + b

add(1, 2) # 正确
# add("1", 2) # TypeError: 参数 'a' 应该是 <class 'int'>...

6. 限流装饰器

限制函数调用频率:

import functools
import time
from collections import deque

def rate_limit(calls, period):
"""限流装饰器

Args:
calls: 在 period 秒内最多调用次数
period: 时间窗口(秒)
"""
def decorator(func):
call_times = deque()

@functools.wraps(func)
def wrapper(*args, **kwargs):
now = time.time()

# 移除时间窗口外的调用记录
while call_times and call_times[0] <= now - period:
call_times.popleft()

# 检查是否超过限制
if len(call_times) >= calls:
wait_time = period - (now - call_times[0])
raise RuntimeError(
f"调用过于频繁,请等待 {wait_time:.1f} 秒"
)

call_times.append(now)
return func(*args, **kwargs)

return wrapper
return decorator

@rate_limit(calls=3, period=10)
def limited_api():
return "API 响应"

类装饰器

装饰器不仅可以用于函数,也可以用于类。类装饰器有两种形式:用函数装饰类,用类实现装饰器。

用函数装饰类

def add_method(cls):
"""为类添加方法"""
def new_method(self):
return f"这是动态添加的方法,来自 {self.__class__.__name__}"

cls.new_method = new_method
return cls

@add_method
class MyClass:
def __init__(self, name):
self.name = name

obj = MyClass("测试")
print(obj.new_method()) # 这是动态添加的方法,来自 MyClass

单例模式装饰器

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):
print("创建数据库连接")

def query(self, sql):
return f"执行: {sql}"

# 无论创建多少次,都是同一个实例
db1 = Database() # 创建数据库连接
db2 = Database() # 不会再次创建
print(db1 is db2) # True

用类实现装饰器

类可以用来实现更复杂的装饰器,因为它可以保存状态:

import functools

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} 次调用 {self.func.__name__}")
return self.func(*args, **kwargs)

@CountCalls
def say_hello():
print("Hello!")

say_hello() # 第 1 次调用 say_hello
say_hello() # 第 2 次调用 say_hello
say_hello() # 第 3 次调用 say_hello

类装饰器的原理是 __call__ 方法。当实例被"调用"时,会执行 __call__ 方法。

装饰器堆叠

多个装饰器可以叠加使用,执行顺序是"从下到上":

def decorator_a(func):
def wrapper():
print("A 开始")
func()
print("A 结束")
return wrapper

def decorator_b(func):
def wrapper():
print("B 开始")
func()
print("B 结束")
return wrapper

@decorator_a
@decorator_b
def my_function():
print("函数执行")

my_function()
# 输出:
# A 开始
# B 开始
# 函数执行
# B 结束
# A 结束

理解执行顺序

@decorator_a
@decorator_b
def my_function():
pass

# 等价于:
my_function = decorator_a(decorator_b(my_function))

装饰器从下往上应用,所以 decorator_b 先包装原函数,然后 decorator_a 再包装结果。调用时,最外层的装饰器先执行。

内置装饰器

Python 提供了几个常用的内置装饰器。

@property

将方法转换为只读属性:

class Circle:
def __init__(self, radius):
self._radius = radius

@property
def radius(self):
"""获取半径"""
return self._radius

@radius.setter
def radius(self, value):
"""设置半径,带验证"""
if value < 0:
raise ValueError("半径不能为负")
self._radius = value

@property
def area(self):
"""计算面积(只读属性)"""
return 3.14159 * self._radius ** 2

circle = Circle(5)
print(circle.radius) # 5(像访问属性一样调用方法)
print(circle.area) # 78.53975

circle.radius = 10 # 使用 setter
# circle.area = 100 # 错误!area 是只读的

@classmethod

定义类方法,第一个参数是类本身:

class Person:
def __init__(self, name, age):
self.name = name
self.age = age

@classmethod
def from_birth_year(cls, name, birth_year):
"""根据出生年份创建实例"""
import datetime
age = datetime.datetime.now().year - birth_year
return cls(name, age)

@classmethod
def adult(cls):
"""返回成人年龄标准"""
return cls("标准成人", 18)

# 使用类方法创建实例
person = Person.from_birth_year("张三", 1990)
print(person.name, person.age)

# 类方法也可以被实例调用
adult_standard = person.adult()

@staticmethod

定义静态方法,不需要访问实例或类:

class Math:
@staticmethod
def add(a, b):
return a + b

@staticmethod
def is_even(n):
return n % 2 == 0

# 通过类调用
print(Math.add(3, 5)) # 8
print(Math.is_even(10)) # True

# 也可以通过实例调用
math = Math()
print(math.add(1, 2)) # 3

@functools.lru_cache

缓存函数结果(Least Recently Used):

from functools import lru_cache

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

print(fibonacci(100)) # 瞬间完成

# 查看缓存统计
print(fibonacci.cache_info())
# CacheInfo(hits=99, misses=101, maxsize=128, currsize=101)

# 清除缓存
fibonacci.cache_clear()

@functools.total_ordering

自动生成比较方法:

from functools import total_ordering

@total_ordering
class Money:
def __init__(self, amount):
self.amount = amount

def __eq__(self, other):
return self.amount == other.amount

def __lt__(self, other):
return self.amount < other.amount
# 只需要定义 __eq__ 和 __lt__,其他比较方法会自动生成

m1 = Money(100)
m2 = Money(200)

print(m1 < m2) # True
print(m1 <= m2) # True(自动生成)
print(m1 > m2) # False(自动生成)

@contextlib.contextmanager

简化上下文管理器的创建:

from contextlib import contextmanager

@contextmanager
def managed_file(name, mode):
"""自动管理文件资源"""
f = open(name, mode)
try:
yield f
finally:
f.close()

with managed_file("test.txt", "w") as f:
f.write("Hello, World!")

装饰器的常见陷阱

陷阱一:忘记返回值

# 错误示例
def bad_decorator(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs) # 忘记返回结果!
return wrapper

# 正确示例
def good_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) # 返回结果
return wrapper

陷阱二:丢失函数元信息

# 错误示例
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@bad_decorator
def example():
"""文档字符串"""
pass

print(example.__name__) # wrapper
print(example.__doc__) # None

# 正确示例:使用 @functools.wraps
import functools

def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

陷阱三:装饰器副作用

装饰器在定义时执行,可能导致意外行为:

# 危险示例:配置文件可能不存在
config = {}

def load_config(func):
# 装饰器定义时就执行,config 可能还未初始化
if 'timeout' not in config:
config['timeout'] = 30
...

陷阱四:递归装饰器

如果装饰器递归调用被装饰的函数,可能导致无限循环:

# 危险示例
def bad_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs) + wrapper(*args, **kwargs) # 无限递归!
return wrapper

装饰器最佳实践

1. 始终使用 functools.wraps

除非有特殊原因,否则总是使用 @functools.wraps 保留原函数的元信息。

2. 保持装饰器简单

装饰器应该只做一件事,保持职责单一:

# 好的做法:职责单一
@timer
@log
@retry(max_attempts=3)
def fetch_data():
...

# 不好的做法:一个装饰器做太多事
@timer_log_retry(max_attempts=3)
def fetch_data():
...

3. 提供有意义的文档

def retry(max_attempts=3, delay=1):
"""自动重试装饰器

当函数抛出异常时自动重试,直到成功或达到最大尝试次数。

Args:
max_attempts: 最大尝试次数,默认 3
delay: 每次重试之间的延迟秒数,默认 1

Returns:
被装饰函数的返回值

Raises:
最后一次尝试抛出的异常
"""
...

4. 考虑性能影响

装饰器会增加函数调用的开销,在性能敏感的场景需要谨慎使用:

import functools
import time

def measure_overhead(func):
"""测量装饰器开销"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def original():
return 1

@measure_overhead
def decorated():
return 1

# 性能对比
start = time.perf_counter()
for _ in range(1_000_000):
original()
print(f"原始函数: {time.perf_counter() - start:.4f}秒")

start = time.perf_counter()
for _ in range(1_000_000):
decorated()
print(f"装饰后函数: {time.perf_counter() - start:.4f}秒")

5. 使用类型注解

from typing import Callable, TypeVar, Any
import functools

F = TypeVar('F', bound=Callable[..., Any])

def retry(max_attempts: int = 3) -> Callable[[F], F]:
"""带类型注解的装饰器"""
def decorator(func: F) -> F:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
...
return wrapper # type: ignore
return decorator

小结

本章深入学习了 Python 装饰器:

核心概念

  • 装饰器是一个函数,接收函数并返回新函数
  • @decorator 是语法糖,等价于 func = decorator(func)
  • 装饰器在函数定义时执行,而不是调用时

关键技术

  • 使用 *args**kwargs 处理任意参数
  • 使用 @functools.wraps 保留函数元信息
  • 使用闭包实现带参数的装饰器

实用场景

  • 日志记录、性能计时、结果缓存
  • 权限验证、限流控制、自动重试
  • 类型检查、参数验证

最佳实践

  • 保持装饰器职责单一
  • 始终使用 @functools.wraps
  • 提供清晰的文档和类型注解

练习

  1. 实现一个 @delay(seconds) 装饰器,延迟执行函数
  2. 实现一个 @throttle(seconds) 装饰器,在指定时间内只执行一次
  3. 实现一个 @validate(**types) 装饰器,验证参数和返回值类型
  4. 使用类装饰器实现一个调用计数器,可以重置计数
  5. 实现一个 @deprecated(message) 装饰器,在函数被调用时打印警告信息

参考资料