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!
# 调用函数之后
执行过程分析:
my_decorator(say_hello)被调用,传入原始的say_hello函数my_decorator内部定义了wrapper函数wrapper函数内部引用了func(通过闭包)my_decorator返回wrapper函数say_hello现在指向wrapper函数- 调用
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!
执行过程:
@repeat(times=3)先调用repeat(3),返回decorator函数- 然后执行
say_hello = decorator(say_hello) - 最终
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 - 提供清晰的文档和类型注解
练习
- 实现一个
@delay(seconds)装饰器,延迟执行函数 - 实现一个
@throttle(seconds)装饰器,在指定时间内只执行一次 - 实现一个
@validate(**types)装饰器,验证参数和返回值类型 - 使用类装饰器实现一个调用计数器,可以重置计数
- 实现一个
@deprecated(message)装饰器,在函数被调用时打印警告信息