Python怎么编写一个装饰器_Python装饰器原理与实战开发

Python装饰器核心是函数作为一等公民和闭包机制,通过@语法在不修改原函数代码的情况下为其添加新功能,如日志、权限控制、缓存等,提升代码复用性和可维护性。

python怎么编写一个装饰器_python装饰器原理与实战开发

Python装饰器,说白了,就是一种特殊函数,它能接收一个函数作为输入,然后给这个函数增加一些额外功能,最终返回一个全新的函数。它就像给你的老朋友穿上了一件新衣服,朋友还是那个朋友,但现在他可能更酷、更强大了,而且这一切都发生在不改变朋友本身代码的前提下。用起来很方便,一个

@

符号就能搞定。

解决方案

编写一个Python装饰器,核心在于理解函数作为一等公民的特性以及闭包的概念。最基础的装饰器,通常是一个接收函数、定义一个内部包装函数、然后返回这个包装函数的高阶函数。

我们先从一个最简单的例子开始。假设我们想在每次调用某个函数之前和之后都打印一些信息,但又不想每次都手动加

print

import functoolsdef log_calls(func):    """    这是一个简单的装饰器,用于记录函数被调用的信息。    """    @functools.wraps(func) # 这一行很重要,它能保留原函数的元信息,比如函数名、文档字符串等。    def wrapper(*args, **kwargs):        print(f"--- 函数 '{func.__name__}' 即将被调用 ---")        # 执行原函数        result = func(*args, **kwargs)        print(f"--- 函数 '{func.__name__}' 调用完毕,返回值为: {result} ---")        return result    return wrapper# 现在,我们用这个装饰器来装饰一个函数@log_callsdef add(a, b):    """一个简单的加法函数。"""    print(f"正在执行 add({a}, {b})")    return a + b@log_callsdef greet(name, greeting="Hello"):    """向指定的人打招呼。"""    print(f"正在执行 greet('{name}', '{greeting}')")    return f"{greeting}, {name}!"# 调用被装饰的函数print("调用 add(5, 3):")sum_result = add(5, 3)print(f"add 函数的最终结果是: {sum_result}n")print("调用 greet('Alice'):")greet_result = greet("Alice")print(f"greet 函数的最终结果是: {greet_result}n")print("调用 greet('Bob', greeting='Hi'):")greet_result_hi = greet("Bob", greeting="Hi")print(f"greet 函数的最终结果是: {greet_result_hi}n")# 如果没有 @log_calls 语法糖,手动装饰是这样的:# original_add = add# add = log_calls(original_add)# print(add(1, 2))

在这个例子里,

log_calls

就是我们的装饰器。它接收一个函数

func

,内部定义了一个

wrapper

函数,

wrapper

函数负责在调用

func

前后添加逻辑,并最终返回

func

的执行结果。最后,

log_calls

返回这个

wrapper

函数。当我们把

@log_calls

放在

add

函数上方时,Python解释器其实做了一件等价于

add = log_calls(add)

的事情,也就是说,

add

这个名字现在指向的不再是原来的加法函数,而是

log_calls

返回的那个

wrapper

函数。

立即学习“Python免费学习笔记(深入)”;

functools.wraps

非常关键,它能把原函数的一些重要元信息(比如

__name__

__doc__

__module__

等)复制到

wrapper

函数上。如果没有它,当你调试或者查看

add.__name__

时,你会发现它变成了

wrapper

,而不是

add

,这会给调试带来不小的麻烦。

Python装饰器的核心工作原理是什么?

在我看来,理解Python装饰器的核心,主要抓住两点:函数是“一等公民”闭包

首先,Python中的函数是“一等公民”(First-Class Citizen)。这意味着函数可以像普通变量一样被赋值给其他变量,可以作为参数传递给其他函数,也可以作为其他函数的返回值。正是因为这个特性,我们才能把一个函数(被装饰的函数)传给另一个函数(装饰器),让装饰器对它进行操作。

其次,也是更关键的一点,是闭包(Closure)。当我们定义

log_calls

装饰器时,它内部的

wrapper

函数引用了外部

log_calls

函数的参数

func

。当

log_calls

执行完毕并返回

wrapper

时,即使

log_calls

的局部作用域已经消失,

wrapper

函数仍然能够“记住”并访问到它被创建时所处的环境中的

func

变量。这种机制就是闭包。

所以,当Python解释器看到

@log_calls

装饰器语法糖时,它会做以下几步:

定义时执行:

add

函数被定义时,

log_calls(add)

会被立即调用。返回包装函数:

log_calls

函数执行,它接收

add

作为参数

func

,然后定义并返回一个名为

wrapper

的新函数。替换原函数: 此时,

add

这个名字不再指向原始的

add

函数,而是指向

log_calls

返回的那个

wrapper

函数。调用时执行: 当你之后调用

add(5, 3)

时,实际执行的是

wrapper(5, 3)

wrapper

函数内部会先打印一些信息,然后通过闭包机制访问到它“记住”的原始

add

函数,并调用它,获取结果,最后再打印一些信息,并将结果返回。

整个过程巧妙地实现了在不修改原函数代码的情况下,为其添加新功能,这在很多场景下都非常有用,比如日志、权限控制、性能测量等等。

装饰器在实际项目中能解决哪些常见问题

实际开发中,装饰器简直是“万金油”,能优雅地解决很多跨领域、重复性的问题。它避免了代码的重复,让业务逻辑更聚焦。

日志记录与调试 (Logging & Debugging): 这是最常见的用途。我们经常需要知道哪个函数在什么时候被调用了,传入了什么参数,返回了什么结果,或者执行了多久。一个通用的日志装饰器能完美解决这些需求,而不需要在每个函数内部都写一堆

print

logging

语句。

import timeimport logginglogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')def log_and_time(func):    @functools.wraps(func)    def wrapper(*args, **kwargs):        logging.info(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")        start_time = time.time()        result = func(*args, **kwargs)        end_time = time.time()        logging.info(f"{func.__name__} finished in {end_time - start_time:.4f}s. Result: {result}")        return result    return wrapper@log_and_timedef complex_calculation(x, y):    time.sleep(0.1) # 模拟耗时操作    return x * y + 10complex_calculation(10, 20)

权限校验与认证 (Authentication & Authorization): 在Web应用中,很多视图函数都需要检查用户是否已登录,或者是否有足够的权限来访问某个资源。把这些检查逻辑封装成装饰器,能让视图函数专注于处理业务逻辑,而不是权限验证

def requires_admin(func):    @functools.wraps(func)    def wrapper(user, *args, **kwargs):        if not user.is_authenticated:            raise PermissionError("User not logged in.")        if not user.is_admin:            raise PermissionError("User does not have admin privileges.")        return func(user, *args, **kwargs)    return wrapperclass User:    def __init__(self, name, authenticated=False, admin=False):        self.name = name        self.is_authenticated = authenticated        self.is_admin = admin@requires_admindef delete_user_data(current_user, user_id):    print(f"Admin '{current_user.name}' deleting data for user {user_id}")    return True# try:#     admin_user = User("Alice", authenticated=True, admin=True)#     delete_user_data(admin_user, 123)#     guest_user = User("Bob", authenticated=True, admin=False)#     delete_user_data(guest_user, 456)# except PermissionError as e:#     print(e)

缓存 (Caching): 对于那些计算成本高昂且结果相对稳定的函数,我们可以用装饰器来缓存其返回值。当函数再次被相同的参数调用时,直接返回缓存结果,避免重复计算。

from functools import lru_cache@lru_cache(maxsize=None) # Python内置的LRU缓存装饰器def fibonacci(n):    if n < 2:        return n    return fibonacci(n-1) + fibonacci(n-2)# 第一次计算会比较慢print(fibonacci(30))# 第二次计算(相同参数)会非常快,因为结果已被缓存print(fibonacci(30))

重试机制 (Retry Mechanism): 当调用外部服务或执行可能失败的操作时,我们可能需要自动重试几次。一个重试装饰器可以优雅地处理这种场景。

import timeimport randomdef retry(max_attempts=3, delay=1):    def decorator_retry(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:                    print(f"Attempt {attempt} failed: {e}")                    if attempt < max_attempts:                        time.sleep(delay)            raise Exception(f"Function {func.__name__} failed after {max_attempts} attempts.")        return wrapper    return decorator_retry@retry(max_attempts=5, delay=0.5)def unstable_api_call():    if random.random() < 0.7: # 70%的几率失败        raise ConnectionError("Simulated API connection error.")    return "Data fetched successfully!"# print(unstable_api_call()) # 尝试调用,可能会重试几次

参数验证 (Argument Validation): 在函数内部对参数进行类型或值检查,可以用装饰器来集中处理,保持函数体的简洁。

这些例子只是冰山一角,装饰器在Web框架(如Flask、Django路由装饰器)、ORM(如SQLAlchemy的事件监听)、以及各种库中都扮演着重要角色,极大地提高了代码的复用性和可维护性。

编写带参数的装饰器有哪些技巧和注意事项?

带参数的装饰器,相比不带参数的,多了一层嵌套。这其实是理解装饰器更深一步的关键。

当装饰器本身需要接收参数时,它就不能直接返回

wrapper

函数了。它需要先接收自己的参数,然后返回一个真正的装饰器函数,这个装饰器函数再接收被装饰的函数,最后返回

wrapper

。听起来有点绕,但看代码就清楚了。

import functoolsdef repeat(num_times):    """    一个带参数的装饰器,让被装饰的函数重复执行指定次数。    `num_times` 是装饰器自身的参数。    """    def decorator_repeat(func): # 这一层是真正的装饰器,它接收被装饰的函数        @functools.wraps(func)        def wrapper(*args, **kwargs): # 这一层是包装函数,它执行原函数            print(f"--- 函数 '{func.__name__}' 将重复执行 {num_times} 次 ---")            results = []            for i in range(num_times):                print(f"  执行第 {i+1} 次...")                result = func(*args, **kwargs)                results.append(result)            print(f"--- 函数 '{func.__name__}' 执行完毕 ---")            # 通常只返回最后一次执行的结果,或者根据需求返回所有结果            return results[-1] if results else None        return wrapper    return decorator_repeat # 装饰器函数返回的是真正的装饰器@repeat(num_times=3) # 这里 num_times=3 就是装饰器的参数def say_hello(name):    print(f"Hello, {name}!")    return f"Hello result for {name}"print("调用 say_hello('Charlie'):")final_result = say_hello("Charlie")print(f"say_hello 函数的最终结果是: {final_result}n")@repeat(num_times=2)def calculate_power(base, exp):    res = base ** exp    print(f"{base}^{exp} = {res}")    return resprint("调用 calculate_power(2, 3):")power_result = calculate_power(2, 3)print(f"calculate_power 函数的最终结果是: {power_result}n")

技巧和注意事项:

多一层嵌套: 最直观的变化就是多了一层函数嵌套。外层函数

repeat

接收装饰器的参数(如

num_times

),它返回的是内层函数

decorator_repeat

decorator_repeat

才是那个接收被装饰函数

func

的“真正的”装饰器。

执行时机:

@repeat(num_times=3)

这行代码,首先会执行

repeat(num_times=3)

repeat

函数会立即执行,并返回

decorator_repeat

这个函数对象。然后,Python解释器再用

decorator_repeat

去装饰

say_hello

函数,即执行

say_hello = decorator_repeat(say_hello)

。所以,带参数的装饰器,它的参数是在定义被装饰函数时就确定了的,而不是在调用被装饰函数时才确定。

functools.wraps

的位置:

functools.wraps(func)

应该放在最内层的

wrapper

函数上,因为它负责将

func

的元信息复制到最终被返回的

wrapper

函数上。

参数传递: 装饰器参数(如

num_times

)通过闭包机制,被

decorator_repeat

wrapper

函数“记住”并使用。

类作为装饰器: 除了函数,类也可以作为装饰器。如果一个类实现了

__call__

方法,它就可以被用作装饰器。当类被用作装饰器时,类的实例就是那个“包装函数”。

class CountCalls:    def __init__(self, func):        functools.update_wrapper(self, func) # 类似 functools.wraps        self.func = func        self.num_calls = 0    def __call__(self, *args, **kwargs):        self.num_calls += 1        print(f"函数 '{self.func.__name__}' 已被调用 {self.num_calls} 次")        return self.func(*args, **kwargs)@CountCallsdef say_whee():    print("Whee!")# say_whee() # 第一次调用# say_whee() # 第二次调用

类装饰器在需要维护状态(如上面的调用次数)时非常方便,因为状态可以直接存储在实例属性中。带参数的类装饰器也同样需要多一层嵌套,即外层函数返回一个类实例。

理解这些,就能更灵活地运用装饰器,为你的Python代码注入更多“魔力”和可维护性。

以上就是Python怎么编写一个装饰器_Python装饰器原理与实战开发的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1372968.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
Python中NumPy计算加速:如何利用多进程避免数据拷贝瓶颈
上一篇 2025年12月14日 12:47:03
Python脚本中执行psql.exe并处理I/O重定向
下一篇 2025年12月14日 12:47:22

相关推荐

  • 修复Django电商项目中AJAX过滤产品列表图片不显示问题

    在Django电商项目中,当使用AJAX动态加载过滤后的产品列表时,常遇到图片无法正常显示的问题。这通常是由于前端模板中图片加载方式(如data-setbg属性结合JavaScript库)与AJAX动态内容更新机制不兼容所致。解决方案是直接在AJAX返回的HTML中使用标准的标签来渲染图片,确保浏览…

    2026年5月10日
    000
  • Matplotlib 地图中多类型图例的创建与优化

    Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化

    本教程旨在解决matplotlib地图可视化中,如何在一个图例中同时展示颜色块(如区域分类)和自定义标记(如特定兴趣点)的问题。文章详细介绍了当传统`patch`对象无法正确显示标记时,如何利用`matplotlib.lines.line2d`创建标记图例句柄,并将其与颜色块图例句柄合并,从而生成一…

    2026年5月10日 用户投稿
    100
  • Golang JSON序列化:控制敏感字段暴露的最佳实践

    本教程探讨golang中如何高效控制结构体字段在json序列化时的可见性。当需要将包含敏感信息的结构体数组转换为json响应时,通过利用`encoding/json`包提供的结构体标签,特别是`json:”-“`,可以轻松实现对特定字段的忽略,从而避免敏感数据泄露,确保api…

    2026年5月10日
    000
  • 利用海象运算符简化条件赋值:Python教程与最佳实践

    本文旨在探讨Python中海象运算符(:=)在条件赋值场景下的应用。通过对比传统if/else语句与海象运算符,以及条件表达式,分析海象运算符在简化代码、提高可读性方面的优势与局限性。并通过具体示例,展示如何在列表推导式等场景下合理使用海象运算符,同时强调其潜在的复杂性及替代方案,帮助开发者更好地掌…

    2026年5月10日
    000
  • 比特币新手教程 比特币交易平台有哪些

    比特币是一种去中心化的数字货币,基于区块链技术实现点对点交易,具有匿名性、有限发行和不可篡改等特点;新手可通过交易所购买,P2P交易获得比特币,常用平台包括Binance、OKX和Huobi;交易流程包括注册账户、实名认证、绑定支付方式、充值法币并下单购买,可选择市价单或限价单;比特币存储方式有交易…

    2026年5月10日
    000
  • c++中的SFINAE技术是什么_c++模板编程中的SFINAE原理与应用

    SFINAE 是“替换失败不是错误”的原则,指模板实例化时若参数替换导致错误,只要存在其他合法候选,编译器不报错而是继续重载决议。它用于条件启用模板、类型检测等场景,如通过 decltype 或 enable_if 控制函数重载,实现类型特征判断。尽管 C++20 引入 Concepts 简化了部分…

    2026年5月10日
    000
  • Golang gRPC流式请求异常处理

    在Golang的gRPC流式通信中,必须通过context.Context处理异常。应监听上下文取消或超时,及时释放资源,设置合理超时,避免连接长时间挂起,并在goroutine中通过context控制生命周期。 在使用 Golang 和 gRPC 实现流式通信时,异常处理是确保服务健壮性的关键部分…

    2026年5月10日
    000
  • Go语言mgo查询构建:深入理解bson.M与日期范围查询的正确实践

    本文旨在解决go语言mgo库中构建复杂查询时,特别是涉及嵌套`bson.m`和日期范围筛选的常见错误。我们将深入剖析`bson.m`的类型特性,解释为何直接索引`interface{}`会导致“invalid operation”错误,并提供一种推荐的、结构清晰的代码重构方案,以确保查询条件能够正确…

    2026年5月10日
    100
  • vscode上怎么运行html_vscode上运行html步骤【指南】

    首先保存文件为.html格式,再通过浏览器或Live Server插件打开预览;推荐安装Live Server实现本地服务器运行与实时刷新,提升开发体验。 在 VS Code 上运行 HTML 文件并不需要复杂的配置,只需几个简单步骤即可预览页面效果。VS Code 本身是一个代码编辑器,不直接运行…

    2026年5月10日
    100
  • RichHandler与Rich Progress集成:解决显示冲突的教程

    在使用rich库的`richhandler`进行日志输出并同时使用`progress`组件时,可能会遇到显示错乱或溢出问题。这通常是由于为`richhandler`和`progress`分别创建了独立的`console`实例导致的。解决方案是确保日志处理器和进度条组件共享同一个`console`实例…

    2026年5月10日
    000
  • Golang goroutine与channel调试技巧

    使用go run -race检测数据竞争,结合runtime.NumGoroutine监控协程数量,通过pprof分析阻塞调用栈,利用select超时避免永久阻塞,有效排查goroutine泄漏、死锁和数据竞争问题。 Go语言的goroutine和channel是并发编程的核心,但它们也带来了调试上…

    2026年5月10日
    000
  • 《魔兽世界》将于6月11日开启国服回归技术测试

    《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试

    《%ign%ignore_a_1%re_a_1%》官方宣布,将于6月11日开启国服回归技术测试,时间为7天,并称可以在6月内正式开服,玩家们可以访问官网下载战网客户端并预下载“巫妖王之怒”客户端,技术测试详情见下图。 WordAi WordAI是一个AI驱动的内容重写平台 53 查看详情 以上就是《…

    2026年5月10日 用户投稿
    200
  • 使用 Jupyter Notebook 进行探索性数据分析

    Jupyter Notebook通过单元格实现代码与Markdown结合,支持数据导入(pandas)、清洗(fillna)、探索(matplotlib/seaborn可视化)、统计分析(describe/corr)和特征工程,便于记录与分享分析过程。 Jupyter Notebook 是进行探索性…

    2026年5月10日
    000
  • php常量怎么用_PHP常量(define/const)定义与使用方法

    PHP中可通过define函数和const关键字定义常量,用于存储不可变值。define适用于全局作用域,支持动态名称和条件定义,如define(‘SITE_NAME’, ‘MyWebsite’);const在编译时生效,语法简洁但限制多,只能在类或全…

    2026年5月10日
    000
  • 如何在HTML中插入表单元素_HTML表单控件与输入类型使用指南

    HTML表单通过标签构建,包含action和method属性定义数据提交目标与方式,常用input类型如text、password、email等适配不同输入需求,配合label、required、placeholder提升可用性,结合textarea、select、button等控件实现完整交互,是…

    2026年5月10日
    000
  • 深入理解 Express.js 中 next() 参数的作用与中间件机制

    本文深入探讨 express.js 中间件函数中的 `next()` 参数。它负责将控制权传递给请求-响应周期中的下一个中间件或路由处理程序。文章将详细解释 `next()` 的工作原理、中间件的注册与执行顺序,以及不正确使用 `next()` 可能导致请求挂起的风险,并通过代码示例和实际应用场景,…

    2026年5月10日
    000
  • 创建指定大小并填充特定数据的Golang文件教程

    本文将介绍如何使用Golang创建一个指定大小的文件,并用特定数据填充它。我们将使用 `os` 包提供的函数来创建和截断文件,从而实现快速生成大文件的目的。示例代码展示了如何创建一个10MB的文件,并将其填充为全零数据。掌握这些方法,可以方便地在例如日志系统或磁盘队列等场景中,预先创建测试文件或初始…

    2026年5月10日
    000
  • Python命令怎样使用profile分析脚本性能 Python命令性能分析的基础教程

    使用Python的cProfile模块分析脚本性能最直接的方式是通过命令行执行python -m cProfile your_script.py,它会输出每个函数的调用次数、总耗时、累积耗时等关键指标,帮助定位性能瓶颈;为进一步分析,可将结果保存为文件python -m cProfile -o ou…

    2026年5月10日
    000
  • 如何插入查询结果数据_SQL插入Select查询结果方法

    如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法

    使用INSERT INTO…SELECT语句可高效插入数据,通过NOT EXISTS、LEFT JOIN、MERGE语句或唯一约束避免重复;表结构不一致时可通过别名、类型转换、默认值或计算字段处理;结合存储过程可提升可维护性,支持参数化与动态SQL。 将查询结果数据插入到另一个表中,可以…

    2026年5月10日 用户投稿
    000
  • 使用 WebCodecs VideoDecoder 实现精确逐帧回退

    本文档旨在解决在使用 WebCodecs VideoDecoder 进行视频解码时,实现精确逐帧回退的问题。通过比较帧的时间戳与目标帧的时间戳,可以避免渲染中间帧,从而提高用户体验。本文将提供详细的解决方案和示例代码,帮助开发者实现精确的视频帧控制。 在使用 WebCodecs VideoDecod…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信