news 2026/9/6 3:24:55

Python 装饰器完全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 装饰器完全指南

一、核心概念

1. 什么是装饰器

装饰器(Decorator)本质上是一个高阶函数——它接收一个函数作为参数,并返回一个新的函数。它允许你在不修改原函数代码和调用方式的前提下,为函数动态添加额外功能

2. 设计原则

装饰器遵循开放-封闭原则

  • 对扩展开放:允许新增功能

  • 对修改封闭:不需要修改原函数内部代码

3. 语法糖

使用@decorator_name放在函数定义上方,是以下写法的简洁形式:

@decorator def func(): pass # 等价于 def func(): pass func = decorator(func)

二、前置知识

要理解装饰器,必须先掌握两个概念:

1. 函数是一等公民

在 Python 中,函数可以像变量一样被赋值、作为参数传递、作为返回值。这是装饰器存在的基础。

2. 闭包(Closure)

在一个内部函数中,对外部作用域(非全局作用域)的变量进行引用,这个内部函数就是闭包。装饰器的核心实现正是利用了闭包的特性。


三、工作原理解析

def my_decorator(func): def wrapper(*args, **kwargs): # 执行前的操作 print("调用函数前") result = func(*args, **kwargs) # 执行原函数 # 执行后的操作 print("调用函数后") return result return wrapper @my_decorator def say_hello(name): print(f"Hello, {name}!") say_hello("Alice") # 输出: # 调用函数前 # Hello, Alice! # 调用函数后

执行流程:

  1. Python 解释器遇到@my_decorator
  2. 执行say_hello = my_decorator(say_hello)
  3. 现在say_hello实际指向wrapper函数
  4. 调用时执行wrapper中的逻辑

四、关键细节:functools.wraps

使用装饰器后,原函数的元信息(如__name____doc__)会被wrapper覆盖,影响调试和文档生成。

解决方案:使用functools.wraps

from functools import wraps def my_decorator(func): @wraps(func) # 保留原函数的 __name__ 和 docstring def wrapper(*args, **kwargs): """这是 wrapper 的文档""" return func(*args, **kwargs) return wrapper @my_decorator def hello(): """这是 hello 的文档""" pass print(hello.__name__) # 输出: hello (没有 wraps 则输出 wrapper) print(hello.__doc__) # 输出: 这是 hello 的文档

五、典型案例

案例 1:计时装饰器(性能分析)

快速测量函数执行时间,是最常见的应用场景之一。

import time 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"[TIMER] {func.__name__} 耗时: {elapsed:.4f} 秒") return result return wrapper @timer def slow_function(): time.sleep(1) return "完成" slow_function() # 输出: [TIMER] slow_function 耗时: 1.0002 秒

案例 2:日志记录装饰器

记录函数的调用信息和返回值,是最常见的应用场景之一。

import functools def log(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"[LOG] 调用 {func.__name__},参数: {args}, {kwargs}") result = func(*args, **kwargs) print(f"[LOG] {func.__name__} 返回: {result}") return result return wrapper @log def add(a, b): return a + b add(2, 3) # 输出: # [LOG] 调用 add,参数: (2, 3), {} # [LOG] add 返回: 5

案例 3:登录权限验证(Web 开发常见)

检查用户登录态或角色权限,是 Web 开发中的典型用法。

import functools def login_required(func): @functools.wraps(func) def wrapper(user, *args, **kwargs): if not user.get("is_login", False): raise PermissionError("用户未登录,请先登录") return func(user, *args, **kwargs) return wrapper @login_required def view_profile(user): print(f"查看用户: {user['name']} 的个人资料") # 测试 user_a = {"name": "张三", "is_login": True} user_b = {"name": "李四", "is_login": False} view_profile(user_a) # 正常执行 # view_profile(user_b) # 抛出 PermissionError
参数传递原理解析

在这个代码中,wrapper(user, *args, **kwargs)里的userwrapper函数的第一个形参,它不是从别处"继承"来的,而是由调用者在调用被装饰后的函数时传入的实参。

1. 装饰器语法糖展开

# 下面的写法: @login_required def view_profile(user): print(f"查看用户: {user['name']} 的个人资料") # 等价于: def view_profile(user): print(f"查看用户: {user['name']} 的个人资料") view_profile = login_required(view_profile)

2. 执行后的结果

  • login_required(view_profile)执行后,返回了内部定义的wrapper函数

  • 此时变量名view_profile不再指向原来的函数,而是指向了wrapper这个新函数

所以当你最后调用view_profile(user_a)时,实际上调用的是:

wrapper(user_a) # 因为 wrapper 的第一个参数名叫 user,所以 user_a 就传给了它

3.user参数的来源总结

  • 定义来源wrapper函数定义时自己声明的第一个位置参数

  • 值来源:来自于外部调用时传入的第一个实参(在上例中是user_a字典)

4. 为什么要这样设计?

因为被装饰的原函数view_profile本身就需要一个user参数,而装饰器wrapper为了能"替换"原函数并正常工作,必须保持相同的调用签名(即接收同样的参数),然后把这些参数原封不动地传递给原函数func

  • wrapper(user, *args, **kwargs)负责接收所有传入参数

  • func(user, *args, **kwargs)负责把参数传给真正的业务函数

如果原函数有多个参数,比如def view_profile(user, age),那么调用时age就会通过*args**kwargs捕获并传递过去。

补充说明:user在这里只是一个普通的参数名,你可以把它改成uperson,效果完全一样(只要内部一致即可)。它和面向对象中的"继承"没有任何关系。

案例 4:重试机制(处理网络请求)

在网络请求等不稳定场景中,失败后自动重试。

import time import functools def retry(max_attempts=3, delay=1): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts: raise e print(f"第 {attempt} 次失败,{delay}秒后重试...") time.sleep(delay) return None return wrapper return decorator @retry(max_attempts=3, delay=0.5) def unstable_request(): import random if random.random() < 0.7: raise ConnectionError("网络不稳定") return "请求成功" # unstable_request()
代码结构总览

这段代码实现了三层嵌套函数,因为装饰器本身需要接收参数:

retry(max_attempts, delay) → 返回 decorator 函数 decorator(func) → 返回 wrapper 函数 wrapper(*args, **kwargs) → 实际执行逻辑
逐层详细拆解

第1层:retry(max_attempts=3, delay=1)

def retry(max_attempts=3, delay=1): def decorator(func): # ... return decorator
  • 作用:这是装饰器的工厂函数,接收配置参数,返回真正的装饰器

  • 参数说明

    • max_attempts=3:最大尝试次数(默认3次)

    • delay=1:每次重试的等待秒数(默认1秒)

  • 关键点:因为装饰器本身需要参数,所以要多一层函数来接收这些参数

第2层:decorator(func)

def decorator(func): def wrapper(*args, **kwargs): # ... return wrapper
  • 作用:这是真正的装饰器,接收被装饰的函数func,返回包装后的函数wrapper

  • 闭包特性decorator内部可以访问外层retrymax_attemptsdelay参数

第3层:wrapper(*args, **kwargs)

def wrapper(*args, **kwargs): for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts: raise e print(f"第 {attempt} 次失败,{delay}秒后重试...") time.sleep(delay) return None
  • 作用:包装后的函数,包含重试逻辑

逐行解析:

  1. 循环控制for attempt in range(1, max_attempts + 1)attempt从 1 到max_attempts(包含)

  2. 尝试执行try: return func(*args, **kwargs),如果成功直接return结果,不继续循环

  3. 异常处理

    • 捕获任何异常

    • 如果是最后一次尝试(attempt == max_attempts),直接抛出异常raise e

    • 否则打印提示并等待delay秒后继续循环

  4. 兜底返回return None,理论上不会执行到这里,仅作为语法完整性保留

应用到具体函数
@retry(max_attempts=3, delay=0.5) def unstable_request(): import random if random.random() < 0.7: raise ConnectionError("网络不稳定") return "请求成功"

等价于:

def unstable_request(): # ... 原函数代码 unstable_request = retry(max_attempts=3, delay=0.5)(unstable_request) # 等价于: # decorator = retry(max_attempts=3, delay=0.5) # unstable_request = decorator(unstable_request)
完整执行流程示例

假设random.random()返回 0.5(小于0.7,触发异常):

unstable_request() # ↓ # 进入 wrapper,attempt = 1 # try: 执行原函数 → 抛出 ConnectionError # except: attempt=1 != 3,打印 "第 1 次失败,0.5秒后重试..." # 等待 0.5 秒 # ↓ # 进入下一轮循环,attempt = 2 # try: 执行原函数 → 又抛出 ConnectionError # except: attempt=2 != 3,打印 "第 2 次失败,0.5秒后重试..." # 等待 0.5 秒 # ↓ # 进入下一轮循环,attempt = 3 # try: 执行原函数 → 再次抛出 ConnectionError # except: attempt=3 == 3,执行 raise e,抛出异常 # ↓ # 程序崩溃,显示 ConnectionError: 网络不稳定

如果第3次成功了(假设random.random()返回 0.8):

# attempt = 3 # try: 执行原函数 → 返回 "请求成功" # return "请求成功"(直接退出函数) # ↓ # 最终输出:请求成功
关键设计思想

1. 闭包(Closure)

retry → decorator → wrapper

每一层都能访问外层变量,形成了闭包链。

2. 参数传递路径

max_attempts=3, delay=0.5 ↓ decorator 捕获这些参数 ↓ wrapper 使用这些参数控制重试逻辑

3. 透明传递

使用*args**kwargs确保任何参数都能传递给原函数,装饰器不关心具体参数。

4. 异常处理策略

  • 不吞异常:最后尝试失败时,重新抛出原始异常

  • 保留堆栈:使用raise e而不是raise,但raise e会重置堆栈信息(更好的做法是直接raise

改进建议

如果想保留完整的异常堆栈,可以改为:

except Exception as e: if attempt == max_attempts: raise # 不传 e,保留完整堆栈

更健壮的版本可以添加指数退避:

time.sleep(delay * (2 ** (attempt - 1))) # 0.5, 1.0, 2.0...
总结对比
组件职责参数
retry配置工厂max_attempts, delay
decorator装饰器主体func
wrapper包装逻辑*args, **kwargs

这种三层结构是带参数装饰器的标准写法,核心思想是将配置与逻辑分离,使装饰器既灵活又复用性强。

案例 5:缓存计算结果(Memoization)

利用闭包保存状态,避免重复计算,大幅提升性能。

import functools def memoize(func): cache = {} @functools.wraps(func) def wrapper(n): if n not in cache: cache[n] = func(n) return cache[n] return wrapper @memoize def fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(35)) # 第一次稍慢,后续极快

实际项目中可直接使用内置的@functools.lru_cache

案例 6:带参数的装饰器(日志级别控制)

当装饰器本身需要参数时,需要三层嵌套(工厂模式)。

import functools def log(level="INFO"): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"[{level}] 调用函数: {func.__name__}") return func(*args, **kwargs) return wrapper return decorator @log(level="ERROR") def dangerous_operation(): print("执行危险操作") dangerous_operation() # 输出: [ERROR] 调用函数: dangerous_operation

逻辑链条:

  1. @log(level="ERROR")先执行,返回decorator
  2. decorator(dangerous_operation)执行,返回wrapper
  3. dangerous_operation()实际调用的是wrapper

案例 7:类装饰器(单例模式)

用类实现装饰器,可以管理更复杂的状态。

import functools class Singleton: def __init__(self, cls): self.cls = cls self.instance = None def __call__(self, *args, **kwargs): if self.instance is None: self.instance = self.cls(*args, **kwargs) return self.instance @Singleton class Database: def __init__(self): print("初始化数据库连接...") db1 = Database() # 初始化数据库连接... db2 = Database() print(db1 is db2) # True,始终是同一个实例

案例 8:类装饰器(调用计数)

利用__call__方法使实例可调用,实现对函数调用次数的统计。

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.func.__name__} 已被调用 {self.count} 次") return self.func(*args, **kwargs) @CountCalls def greet(name): return f"Hello, {name}!" greet("Alice") greet("Bob") # 输出: # greet 已被调用 1 次 # greet 已被调用 2 次

六、多个装饰器的执行顺序

当多个装饰器叠加时,从下往上"包裹",从上往下"执行"

@timer # 后执行外层(后包裹),先执行外层逻辑 @log(level="DEBUG") # 先被包裹(内层),后执行内层逻辑 def add(a, b): time.sleep(0.1) return a + b # 等价于: add = timer(log(level="DEBUG")(add)) # 执行顺序: 先执行 log 的 wrapper 逻辑,再执行 timer 的 wrapper 逻辑 # 实际输出: # [DEBUG] 调用 add,参数: (3, 5), {} # [DEBUG] add 返回: 8 # [TIMER] add 耗时: 0.1005 秒

七、常见应用场景

场景说明关键技术
日志记录自动记录函数调用信息和参数两层嵌套 +wraps
性能监控统计执行时间、调用次数计时装饰器
权限校验检查用户登录态或角色权限带参数的装饰器
重试与降级失败后自动重试或返回默认值三层嵌套(工厂模式)
缓存减少重复计算,提高响应速度闭包字典存状态
事务管理数据库操作的自动提交/回滚装饰器模式
限流控制函数调用频率装饰器 + 状态管理
单例模式确保类只有一个实例类装饰器

八、Python 标准库中常用的装饰器

装饰器说明
@functools.lru_cache函数结果缓存
@staticmethod定义静态方法
@classmethod定义类方法
@property属性访问控制
@functools.singledispatch基于类型的函数重载
@functools.wraps保留原函数元信息

九、总结

装饰器的核心价值在于分离关注点

  • 核心业务逻辑保持纯粹

  • 横切关注点(日志、验证、缓存、计时等)通过装饰器叠加

掌握装饰器需要注意的关键点:

  1. 本质:装饰器是高阶函数,接收函数,返回新函数
  2. 语法糖@decorator只是func = decorator(func)的简写
  3. 规范:务必使用@functools.wraps(func)保留原函数属性
  4. 参数:处理原函数参数用*args, **kwargs;处理装饰器参数需要多一层嵌套
  5. 场景:日志、计时、缓存、权限校验、重试机制、事务处理等横切关注点

装饰器的精髓在于**"开闭原则"**——对扩展开放,对修改封闭。当你发现多个函数需要重复编写同一段前置/后置逻辑时,就是该用装饰器的时候了。掌握装饰器,你的 Python 代码将从"过程式脚本"迈向"Pythonic 的工程化代码"。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 3:23:07

嵌入式Linux下Modbus RTU主站开发与RS485串口配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 3:21:52

Obsidian 2026 插件推荐:同步、AI 与版本控制一体化工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 3:21:48

车载CAN通信与UDS诊断协议开发:从底层硬件到上层应用全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 3:15:56

2026靠谱白转黑维生素品牌推荐

选购白转黑类维生素产品时首先要明确三个核心避坑红线&#xff0c;第一是宣称7天快速白发转黑的产品绝对不要选&#xff0c;营养素补充剂遵循人体正常代谢规律&#xff0c;不可能实现超常规的激进功效&#xff1b;第二是没有国家营养素补充剂备案号的产品要直接排除&#xff0c…

作者头像 李华
网站建设 2026/9/6 3:15:18

GEO 落地五步法:让品牌成为 AI 的“标准答案“

GEO 落地五步法&#xff1a;让品牌成为 AI 的"标准答案"接触过的企业里&#xff0c;最常见的误区是把 GEO 理解成"换个地方发软文"。于是内容照旧、渠道照旧&#xff0c;只是标题里多了几个问句&#xff0c;三个月后一查&#xff0c;AI 里还是查无此品牌。…

作者头像 李华
网站建设 2026/9/6 3:13:53

FPGA跑通100G UDP协议栈:移植适配与上板测试全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华