一、装饰器概述
装饰器(Decorator)是Python中一种极其强大的设计模式,它允许我们在不修改原有函数或类代码的情况下,为其添加额外的功能。本质上,装饰器是一个接受函数作为参数并返回一个新函数的可调用对象。Python的装饰器语法糖(@decorator)使得代码更加简洁优雅,在日志记录、性能计时、权限校验、缓存等场景中被广泛使用。
核心思想:装饰器遵循"开闭原则"--对扩展开放,对修改关闭。它让我们能够在不修改原有函数定义的前提下,透明地增强函数的行为。这是Python从函数式编程中借鉴的核心理念之一。
装饰器的作用远不止于简单的"包装"。深入理解装饰器,意味着要掌握Python中的一等公民(first-class function)、闭包(closure)、函数内省(introspection)等核心概念。本文将从最基础的原理出发,逐步深入到高级用法和实战模式,帮助读者建立完整的装饰器知识体系。
前置知识:阅读本文需要熟悉Python函数定义、*args和**kwargs可变参数、闭包基本概念。如果对这些概念尚不熟悉,建议先复习相关基础知识。
二、装饰器原理与@语法糖
要理解装饰器,首先要理解Python中函数作为"一等公民"的特性:函数可以赋值给变量、可以作为参数传递给其他函数、也可以作为其他函数的返回值。装饰器的本质就是利用这一特性,对目标函数进行"包装"。
2.1 函数作为对象
在Python中,函数和其他对象一样,可以在运行时创建、赋值、传递:
# 函数可以赋值给变量
def greet(name):
return f"Hello, {name}!"
say_hello = greet # 将函数对象赋值给新变量
print(say_hello("Alice")) # 输出: Hello, Alice!
# 函数可以作为参数传递
def call_twice(func, arg):
return func(arg), func(arg)
result1, result2 = call_twice(greet, "Bob")
print(result1) # 输出: Hello, Bob!
# 函数可以作为返回值(闭包的基础)
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 输出: 10
print(triple(5)) # 输出: 15
2.2 手动实现装饰器
理解装饰器最佳的方式是先不依赖 @ 语法,手动实现装饰逻辑:
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用函数前: {func.__name__}")
result = func(*args, **kwargs)
print(f"调用函数后: {func.__name__}")
return result
return wrapper
def say_hello():
print("你好,世界!")
# 手动应用装饰器(等价于 @my_decorator)
say_hello = my_decorator(say_hello)
say_hello()
# 输出:
# 调用函数前: say_hello
# 你好,世界!
# 调用函数后: say_hello
在上面的例子中,my_decorator 接受一个函数 func,在内部定义了一个新的函数 wrapper,这个 wrapper 函数在调用原始函数前后加入了额外的逻辑,然后返回了 wrapper。当我们执行 say_hello = my_decorator(say_hello) 时,原有的 say_hello 被替换成了包装后的版本。
2.3 @语法糖
Python 提供了 @ 语法糖来简化装饰器的应用,上面例子可以改写为:
@my_decorator
def say_hello():
print("你好,世界!")
# 完全等价于: say_hello = my_decorator(say_hello)
@ 语法糖的本质没有任何特殊之处,它仅仅是语法层面的简化。实际上,在解释器处理到 @my_decorator 时,它会立即执行 my_decorator 并将紧随其后的函数作为参数传入。理解这一点非常重要,因为它决定了装饰器的执行时机。
执行时机:装饰器(@语句)在模块导入(import)时立即执行,而非在装饰后的函数被调用时才执行。这意味着装饰器的设置成本发生在导入阶段,而包装函数内的逻辑才在每次调用时执行。
三、functools.wraps 保留元数据
上面实现的装饰器有一个严重的问题:它破坏了被装饰函数的元数据(metadata)。当我们使用装饰器后,函数的名字、文档字符串、参数签名等信息都会丢失,因为它们被替换成了 wrapper 函数的信息。
def my_decorator(func):
def wrapper(*args, **kwargs):
"""包装函数的文档"""
print(f"调用: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a: int, b: int) -> int:
"""返回两个整数的和"""
return a + b
print(add.__name__) # 输出: wrapper (❌ 应为 add)
print(add.__doc__) # 输出: 包装函数的文档 (❌ 应为 返回两个整数的和)
print(add.__annotations__) # 输出: {} (❌ 应为 {'a': int, 'b': int, 'return': int})
上述问题在很多场景下会造成困扰:调试时看到的是 wrapper 的名字而非原始函数名;使用文档生成工具(如 Sphinx)时文档字符串丢失;某些依赖函数签名的框架可能出错。
解决方案:使用标准库 functools.wraps,它可以将原始函数的元数据复制到 wrapper 函数上:
from functools import wraps
def my_decorator(func):
@wraps(func) # 关键:保留原始函数元数据
def wrapper(*args, **kwargs):
"""包装函数的文档"""
print(f"调用: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a: int, b: int) -> int:
"""返回两个整数的和"""
return a + b
print(add.__name__) # 输出: add ✅
print(add.__doc__) # 输出: 返回两个整数的和 ✅
print(add.__annotations__) # 输出: {'a': int, 'b': int, 'return': int} ✅
最佳实践:每次编写自定义装饰器时,都应在 wrapper 函数上使用 @functools.wraps。这不仅是良好的编码习惯,也能避免许多隐晦的调试问题。有些第三方库(如 Flask、Django)会依赖函数名称和签名进行路由注册,没有 wraps 可能导致难以排查的错误。
functools.wraps 实际上是将原始函数的 __module__、__name__、__qualname__、__annotations__、__doc__、__dict__ 以及 __wrapped__ 属性复制到 wrapper 函数上。__wrapped__ 属性是一个特殊的约定,用于记录原始函数的引用,方便内省工具追踪。
四、带参数装饰器(三层嵌套)
有时候我们希望装饰器本身可以接受参数,从而提供更灵活的行为。例如,一个日志装饰器可能希望指定日志级别:
@log(level="DEBUG")
def process_data():
pass
带参数装饰器需要三层嵌套结构:
- 外层函数:接收装饰器的参数,返回内层函数
- 中层函数:接收被装饰的函数,返回包装函数
- 内层函数(wrapper):接收 *args 和 **kwargs,实现具体包装逻辑
4.1 带参数装饰器的实现
from functools import wraps
def repeat(times=2):
"""带参数的装饰器:重复执行被装饰函数指定次数"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
print(f"第 {i+1} 次调用:")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# 输出:
# 第 1 次调用:
# Hello, Alice!
# 第 2 次调用:
# Hello, Alice!
# 第 3 次调用:
# Hello, Alice!
4.2 语法本质
理解带参数装饰器的关键在于认识到 @repeat(times=3) 的求值过程:
# @repeat(times=3) 等价于两步操作:
# 第一步: temp = repeat(times=3) → 返回 decorator 函数
# 第二步: @temp → 相当于 greet = temp(greet)
# 所以:
@repeat(times=3)
def greet(name): ...
# 等价于:
# greet = repeat(times=3)(greet)
这意味着 repeat(times=3) 先被调用,返回 decorator 函数,然后 decorator 以 greet 为参数被调用,返回包装后的 wrapper 函数。
4.3 同时支持带参数和不带参数
在高级用法中,我们可能希望装饰器同时支持 @decorator 和 @decorator(args) 两种写法。实现这一点的技巧是检查第一个参数是否为函数:
from functools import wraps
def repeat(func=None, *, times=2):
"""同时支持 @repeat 和 @repeat(times=3) 两种用法"""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
for i in range(times):
result = f(*args, **kwargs)
return result
return wrapper
if func is not None:
# 无参数调用: @repeat
return decorator(func)
# 有参数调用: @repeat(times=3)
return decorator
@repeat
def say_hello():
print("你好!")
@repeat(times=3)
def say_goodbye():
print("再见!")
参数设计建议:当装饰器参数较多或可能扩展时,建议强制使用关键字参数(如上例中的 * 分隔符),避免因参数位置混淆导致的错误。同时,这种设计模式也让装饰器的 API 更加清晰。
五、多个装饰器叠加顺序
当多个装饰器叠加在同一个函数上时,它们应用的顺序和执行的顺序是不同的,这是一个常见的混淆点。
5.1 叠加规则
from functools import wraps
def decorator_a(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("进入 A")
result = func(*args, **kwargs)
print("离开 A")
return result
return wrapper
def decorator_b(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("进入 B")
result = func(*args, **kwargs)
print("离开 B")
return result
return wrapper
@decorator_a
@decorator_b
def core():
print("执行核心函数")
core()
上述代码的输出为:
进入 A
进入 B
执行核心函数
离开 B
离开 A
这里的规则可以拆解为两条:
- 应用顺序(装饰顺序):从下往上。即
core 先被 decorator_b 装饰,结果再被 decorator_a 装饰。等价于 core = decorator_a(decorator_b(core))
- 执行顺序(调用顺序):从上往下进入,从下往上退出。类似于洋葱模型的层层包裹,最外层的装饰器先执行前置逻辑,然后层层向内传递,最后从内到外逐层执行后置逻辑。
洋葱模型记忆法:可以把多个装饰器想象成洋葱的层层皮,函数调用就是从外层到内层再到外层的过程。最靠近函数的装饰器(最下面的 @)先包装但最后执行前置逻辑。应用顺序和调用顺序是完全相反的。
5.2 实际应用中的注意事项
在同时使用多个装饰器时,顺序非常重要。常见的最佳实践包括:
- 将开销大的装饰器(如缓存)放在外层,以便尽早返回结果
- 将安全检查装饰器放在最外层,未授权请求尽早拒绝
- 日志和计时装饰器通常放在内层,以精确测量实际执行时间
- 注意装饰器之间的相互影响,某些装饰器可能改变参数或返回值
from functools import wraps
def cache_result(func):
"""缓存装饰器(应靠近函数放置)"""
stored = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
if key not in stored:
stored[key] = func(*args, **kwargs)
return stored[key]
return wrapper
def log_execution(func):
"""日志装饰器(应在外层)"""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"调用: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_execution # 外层:先记录日志
@cache_result # 内层:提供缓存
def expensive_computation(x, y):
"""一个耗时计算"""
from time import sleep
sleep(1)
return x ** y
六、类装饰器(__call__/__init__)
除了函数形式的装饰器,Python 还支持使用类来实现装饰器。类装饰器通过 __init__ 接收被装饰函数,通过 __call__ 使实例成为可调用对象,从而实现包装逻辑。
6.1 类作为装饰器
from functools import wraps
class CountCalls:
"""类装饰器:统计函数调用次数"""
def __init__(self, func):
"""__init__ 接收被装饰的函数"""
self.func = func
self.calls = 0
wraps(func)(self) # 手动复制函数元数据到实例
def __call__(self, *args, **kwargs):
"""使实例可调用,实现包装逻辑"""
self.calls += 1
print(f"{self.func.__name__} 已被调用 {self.calls} 次")
return self.func(*args, **kwargs)
@CountCalls
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice") # 输出: say_hello 已被调用 1 次 \n Hello, Alice!
say_hello("Bob") # 输出: say_hello 已被调用 2 次 \n Hello, Bob!
print(say_hello.calls) # 输出: 2
类装饰器的优势在于可以更方便地维护状态。在上例中,self.calls 就是装饰器维护的状态变量。此外,类装饰器可以轻松添加额外的方法和属性,使装饰器本身具有更丰富的接口。
6.2 带参数的类装饰器
类装饰器也可以带参数,只需在 __init__ 中接收参数,然后将真正的装饰逻辑放在 __call__ 中:
class Retry:
"""类装饰器:失败重试"""
def __init__(self, max_attempts=3, delay=0):
"""接收装饰器参数"""
self.max_attempts = max_attempts
self.delay = delay
def __call__(self, func):
"""接收被装饰函数,返回包装函数"""
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, self.max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"第 {attempt} 次尝试失败: {e}")
if attempt == self.max_attempts:
raise
from time import sleep
sleep(self.delay)
continue
return None
return wrapper
@Retry(max_attempts=3, delay=0.5)
def unstable_network_call():
import random
if random.random() < 0.6:
raise ConnectionError("网络连接失败")
return "成功获取数据"
函数 vs 类装饰器:函数装饰器更简洁,适合逻辑简单的场景;类装饰器更适合需要维护状态的场景(如计数器、缓存池)或需要提供额外方法/属性的场景。在实际开发中,大多数场景使用函数装饰器即可,但当装饰器本身较为复杂时,类装饰器的可读性和可维护性更高。
七、装饰器在类方法上的应用
装饰器在类方法上使用时需要特别小心,因为方法的第一个参数是 self(或 cls 对于类方法)。
7.1 普通装饰器与方法的兼容性
大多数通用装饰器使用 *args, **kwargs 传递参数,天然兼容类方法:
from functools import wraps
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"调用方法: {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Calculator:
@logger
def add(self, a, b):
return a + b
@logger
@staticmethod
def static_multiply(a, b):
return a * b
@logger
@classmethod
def class_info(cls):
return f"Calculator class"
calc = Calculator()
print(calc.add(3, 4)) # 输出: 调用方法: add \n 7
print(Calculator.static_multiply(5, 6)) # 正常
print(Calculator.class_info()) # 正常
7.2 装饰器顺序:@staticmethod/@classmethod 的特殊性
@staticmethod 和 @classmethod 是Python的内置装饰器。它们与其他自定义装饰器的顺序非常重要:自定义装饰器必须放在 @staticmethod 或 @classmethod 的上面:
class Example:
# 正确写法:自定义装饰器在上方
@logger
@staticmethod
def good_example():
return "正确"
# 错误写法:会导致 TypeError
# @staticmethod
# @logger
# def bad_example():
# return "错误"
# 为什么?
# 正确的顺序等价于:
# good_example = logger(staticmethod(good_example))
# 错误顺序等价于:
# bad_example = staticmethod(logger(bad_example))
# 后者会将包装函数(一个普通函数)传递给 staticmethod,
# 导致调用时 self 参数处理异常
7.3 类级别的装饰器
装饰器不仅可以用在函数和方法上,还可以用在类本身。类装饰器接收类作为参数,返回修改后的类:
def add_repr(cls):
"""类装饰器:自动生成 __repr__ 方法"""
def __repr__(self):
attributes = ', '.join(
f"{k}={v!r}" for k, v in self.__dict__.items()
)
return f"{cls.__name__}({attributes})"
cls.__repr__ = __repr__
return cls
def singleton(cls):
"""类装饰器:实现单例模式"""
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
def __init__(self, host="localhost"):
self.host = host
print(f"连接数据库: {host}")
@add_repr
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# 测试
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # 输出: True (单例,只创建一次实例)
p = Point(3, 4)
print(p) # 输出: Point(x=3, y=4) (自动生成 __repr__)
八、wrapt 库实现健壮的装饰器
在生产环境中,手动编写的装饰器可能会遇到一些边界问题:函数签名信息丢失(即使使用了 wraps)、内省工具(如 inspect)可能无法正常工作、装饰器在类方法上可能出现兼容性问题。wrapt 库(pip install wrapt)提供了一套完整的解决方案。
8.1 使用 wrapt 编写装饰器
import wrapt
@wrapt.decorator
def timed(wrapped, instance, args, kwargs):
"""使用 wrapt 实现的计时装饰器"""
import time
start = time.perf_counter()
try:
return wrapped(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
print(f"{wrapped.__name__} 耗时: {elapsed:.4f}秒")
# 在普通函数上使用
@timed
def compute(n):
from time import sleep
sleep(n / 10)
return n ** 2
print(compute(3))
# 输出:
# compute 耗时: 0.3006秒
# 9
# 在类方法上使用
class Service:
@timed
def process(self, data):
from time import sleep
sleep(0.2)
return f"处理结果: {data}"
s = Service()
print(s.process("test"))
# 输出:
# process 耗时: 0.2003秒
# 处理结果: test
8.2 wrapt 的优势
wrapt 库解决了纯函数装饰器的几个关键痛点:
- 透明的函数签名:使用 wrapt 编写的装饰器不会破坏被装饰函数的签名信息,
inspect.signature 可以正确返回原始函数的签名
- 方法兼容性:wrapt 自动处理了 bound method、unbound method、classmethod 和 staticmethod 的差异,无需单独适配
- 内省支持:保留了完整的
__wrapped__ 属性链,方便调试工具进行展开
- 性能优化:wrapt 的 C 扩展实现了高效的函数包装
import wrapt
import inspect
def plain_decorator(func):
"""普通装饰器"""
from functools import wraps
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@wrapt.decorator
def wrapt_decorator(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
@plain_decorator
def f1(a: int, b: str) -> bool:
return True
@wrapt_decorator
def f2(a: int, b: str) -> bool:
return True
# 对比签名保留情况
print(inspect.signature(f1)) # 可能输出: (*args, **kwargs) (签名丢失)
print(inspect.signature(f2)) # 输出: (a: int, b: str) -> bool (签名完整保留 ✅)
什么时候使用 wrapt?如果装饰器仅用于个人项目或简单脚本,使用 functools.wraps 即可。但如果装饰器是一个供他人使用的库函数,或者需要在类方法上使用,或者需要保留完整的函数签名信息,强烈建议使用 wrapt 库。
九、常见应用模式
装饰器在实际开发中有大量成熟的应用模式。掌握这些模式,可以显著提升代码的复用性和可维护性。
9.1 日志记录(Logging)
from functools import wraps
import logging
logging.basicConfig(level=logging.INFO)
def log_call(logger=None):
"""装饰器:记录函数调用的参数和返回值"""
if logger is None:
logger = logging.getLogger(__name__)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
logger.info(f"调用 {func.__name__}({signature})")
try:
result = func(*args, **kwargs)
logger.info(f"{func.__name__} 返回: {result!r}")
return result
except Exception as e:
logger.exception(f"{func.__name__} 抛出异常: {e}")
raise
return wrapper
return decorator
@log_call()
def divide(a, b):
return a / b
divide(10, 2) # 日志: 调用 divide(10, 2) \n divide 返回: 5.0
divide(10, 0) # 日志: 调用 divide(10, 0) \n divide 抛出异常: division by zero
9.2 性能计时(Timing)
import time
from functools import wraps
def timer(unit='ms'):
"""装饰器:测量函数执行时间"""
unit_map = {'s': 1, 'ms': 1000, 'us': 1000000, 'ns': 1000000000}
scale = unit_map.get(unit, 1000)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = (time.perf_counter() - start) * scale
print(f"{func.__name__} 耗时: {elapsed:.2f}{unit}")
return result
return wrapper
return decorator
@timer(unit='ms')
def load_data(size):
from time import sleep
sleep(size / 1000) # 模拟I/O操作
return [i for i in range(size)]
data = load_data(50000)
9.3 失败重试(Retry)
from functools import wraps
import random
def retry(max_attempts=3, allowed_exceptions=(Exception,), backoff=1):
"""装饰器:函数执行失败时自动重试,支持退避策略"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except allowed_exceptions as e:
last_exception = e
print(f"第 {attempt} 次尝试失败: {type(e).__name__}: {e}")
if attempt < max_attempts:
wait = backoff * (2 ** (attempt - 1)) # 指数退避
print(f"等待 {wait} 秒后重试...")
import time
time.sleep(wait)
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, backoff=0.5)
def fetch_data():
"""模拟不稳定的API调用"""
if random.random() < 0.7: # 70% 概率失败
raise ConnectionError("网络超时")
return {"status": "ok", "data": [1, 2, 3]}
result = fetch_data()
print(f"最终结果: {result}")
9.4 缓存结果(Caching)
from functools import wraps, lru_cache
# 方法一:使用标准库 lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
"""计算斐波那契数列(递归版本,未优化时会极慢)"""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 方法二:自定义TTL缓存
import time
def ttl_cache(seconds=60):
"""带过期时间的缓存装饰器"""
def decorator(func):
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
now = time.time()
if key in cache:
result, timestamp = cache[key]
if now - timestamp < seconds:
print(f"缓存命中: {func.__name__}{key}")
return result
else:
print(f"缓存过期,重新计算")
result = func(*args, **kwargs)
cache[key] = (result, now)
return result
return wrapper
return decorator
@ttl_cache(seconds=10)
def get_user_info(user_id):
"""模拟从数据库获取用户信息"""
print(f"正在查询数据库: user_id={user_id}")
return {"id": user_id, "name": f"User_{user_id}"}
print(get_user_info(1)) # 查询数据库
print(get_user_info(1)) # 缓存命中
time.sleep(11)
print(get_user_info(1)) # 缓存过期,重新查询
9.5 权限检查(Authorization)
from functools import wraps
# 模拟用户和权限系统
class User:
def __init__(self, name, roles):
self.name = name
self.roles = roles
def require_role(*required_roles):
"""装饰器:检查用户是否具有所需角色权限"""
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not isinstance(user, User):
raise TypeError("第一个参数必须是 User 实例")
if not any(role in user.roles for role in required_roles):
raise PermissionError(
f"用户 {user.name} 不具有所需权限: {required_roles}"
)
print(f"权限验证通过: {user.name} 拥有 {required_roles} 权限")
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(admin_user, target_user_id):
"""删除用户(需要admin权限)"""
print(f"管理员 {admin_user.name} 删除了用户 {target_user_id}")
return True
@require_role("admin", "editor")
def edit_article(editor_user, article_id):
"""编辑文章(需要admin或editor权限)"""
print(f"编辑 {editor_user.name} 修改了文章 {article_id}")
return True
# 测试
admin = User("张三", ["admin", "editor"])
editor = User("李四", ["editor"])
viewer = User("王五", ["viewer"])
delete_user(admin, 42) # 通过
edit_article(editor, 101) # 通过
edit_article(admin, 102) # 通过
try:
delete_user(viewer, 43) # 抛出 PermissionError
except PermissionError as e:
print(f"权限拒绝: {e}")
9.6 类型检查(Type Checking)
from functools import wraps
def type_check(**expected_types):
"""装饰器:运行时检查参数类型"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
from inspect import signature
# 将位置参数绑定到参数名
bound = signature(func).bind(*args, **kwargs)
bound.apply_defaults()
for param_name, expected_type in expected_types.items():
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"参数 '{param_name}' 期望类型 {expected_type.__name__}, "
f"实际得到 {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@type_check(a=int, b=int)
def add(a, b):
return a + b
print(add(3, 4)) # 输出: 7
try:
add("3", 4) # 抛出 TypeError
except TypeError as e:
print(f"类型错误: {e}")
十、装饰器的性能影响
装饰器虽然强大,但并非没有代价。理解装饰器的性能影响对于编写高效的程序至关重要。
10.1 调用开销
每个装饰器层都会引入额外的函数调用开销。在性能敏感的场景中,多装饰器嵌套可能导致明显的性能下降:
from functools import wraps
import time
def empty_decorator(func):
"""尽可能轻量的装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@empty_decorator
def decorated_func():
return 42
def plain_func():
return 42
# 性能对比
N = 1000000
start = time.perf_counter()
for _ in range(N):
plain_func()
plain_time = time.perf_counter() - start
start = time.perf_counter()
for _ in range(N):
decorated_func()
decorated_time = time.perf_counter() - start
print(f"原始函数: {plain_time:.3f}秒")
print(f"装饰函数: {decorated_time:.3f}秒")
print(f"开销: {(decorated_time/plain_time - 1)*100:.1f}%")
10.2 开销分析
装饰器引入的性能开销主要来自以下几个方面:
- 额外函数调用:每次调用被装饰函数时,都需要先调用 wrapper 函数,然后再调用原始函数。在CPython中,函数调用本身就有一定的开销(参数打包、栈帧创建等)。
- 闭包变量访问:wrapper 函数中对外层变量的访问比局部变量略慢。
- wraps 复制元数据:虽然只在定义时执行一次,但如果装饰器被大量使用,模块导入时的开销也不容忽视。
10.3 优化建议
针对装饰器的性能问题,可以采取以下优化策略:
- 减少装饰器层级:将多个轻量装饰器合并为一个装饰器,可以减少嵌套调用
- 使用 lru_cache 优化:对于纯函数,用
@functools.lru_cache 缓存结果,用空间换时间
- 条件装饰:在非生产环境或调试模式下跳过装饰器
- 内联简单逻辑:对于极简单的增强逻辑,考虑直接修改原函数而非使用装饰器
from functools import wraps
def conditional_decorator(active=True):
"""条件装饰器:根据条件启用或禁用装饰"""
def decorator(func):
if not active:
# 不活跃时直接返回原始函数,零开销
return func
@wraps(func)
def wrapper(*args, **kwargs):
print(f"正在执行: {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
# 生产环境可以设置为 False,完全消除装饰器开销
@conditional_decorator(active=True)
def debug_function():
pass
性能结论:在绝大多数业务应用中,装饰器带来的性能开销(微秒级)可以忽略不计。但如果是高频调用的热点函数(每秒数十万次调用),则需要谨慎使用多层装饰器嵌套,或考虑在关键路径上不使用装饰器。
十一、核心要点总结
1. 装饰器本质:装饰器是接受函数并返回新函数的高阶函数。@语法糖只是简化了语法,本质仍是 func = decorator(func)。
2. 元数据保留:始终使用 @functools.wraps 保留被装饰函数的名称、文档字符串和签名信息。
3. 三层嵌套模式:带参数的装饰器需要外层接收参数、中层接收函数、内层接收调用参数的三层结构。
4. 叠加顺序:多个装饰器从下往上应用(靠近函数的先装饰),执行时则是外层先进入、内层先执行。
5. 类装饰器:通过 __init__ 接收函数,__call__ 实现包装,适合需要维护状态的场景。
6. 方法兼容:自定义装饰器在 @staticmethod/@classmethod 之上,使用 *args, **kwargs 保证兼容性。
7. wrapt库:生产环境中推荐使用 wrapt 库编写装饰器,它能自动处理函数签名保留和方法兼容性问题。
8. 实战模式:日志、计时、重试、缓存、权限检查是装饰器最常用的五个应用模式,建议熟练掌握。
9. 性能注意:装饰器有微小的调用开销,热点函数需谨慎使用多层嵌套,可使用条件装饰器在需要时启用。
十二、进一步思考
装饰器的学习不是终点,而是通往更深层次Python理解的桥梁。在掌握基础装饰器后,可以进一步探索以下方向:
- 元编程与描述符:装饰器是Python元编程能力的一部分,结合描述符协议(
__get__、__set__、__delete__)可以实现更强大的功能,如ORM框架中的字段定义。
- 上下文管理器:装饰器与
with 语句(__enter__、__exit__)解决的是不同层面的问题,但在资源管理方面可以实现互补。
- 异步装饰器:在异步编程中,装饰器需要处理
async def 函数和 await 表达式,这是一个进阶主题。
- 编译时装饰器 vs 运行时装饰器:Python装饰器是纯粹的运行时机制,而其他语言(如Java注解)可能有编译时处理。理解这种区别有助于跨语言对比学习。
- 装饰器在框架中的应用:深入学习 Flask(路由注册)、Django(权限控制)、Click(命令行参数)等框架的装饰器实现,可以加深对装饰器设计模式的理解。
"装饰器模式的核心在于:在不改变现有对象结构的前提下,动态地给对象添加额外的职责。Python的装饰器以语言级特性实现了这一经典设计模式,展示了动态语言的独特魅力。" —— 学习笔记
通过本文的学习,相信读者已经对Python装饰器有了系统而深入的理解。装饰器是Python优雅哲学的典型体现——简洁、强大、富有表现力。在日常编码中恰当地使用装饰器,可以让代码更加模块化、可复用和易于维护。