0

0

python如何实现一个装饰器_python装饰器原理与实现方法详解

穿越時空

穿越時空

发布时间:2025-09-14 22:55:01

|

459人浏览过

|

来源于php中文网

原创

Python装饰器利用函数为一等公民和闭包特性,通过@语法为函数添加功能而不修改其代码。如log_calls装饰器可记录函数调用日志,核心是外部函数返回嵌套的wrapper函数,wrapper保留对原函数的引用并扩展行为。functools.wraps确保被装饰函数的元信息不变。带参数的装饰器需多一层函数嵌套,形成“装饰器工厂”,如timer(unit)返回真正的装饰器。类也可作为装饰器,通过实现__call__方法,在实例中保存状态,适用于需维护调用次数或共享资源的场景,如CallCounter统计函数调用次数。

python如何实现一个装饰器_python装饰器原理与实现方法详解

Python装饰器,说白了,就是一种特殊的函数,它的主要工作是去“包裹”或者说“包装”另一个函数,给这个被包装的函数增加额外的功能,但又不需要我们去直接修改被包装函数的源代码。这听起来有点像给一个礼物盒外面再套一层包装纸,里面的礼物(原函数)还是那个礼物,但外面的包装纸(装饰器)给它增添了新的“仪式感”或者说“功能”。它的核心原理,其实就是利用了Python中函数是“一等公民”的特性,以及闭包(closure)的概念,通过

@
这个语法糖,让代码变得非常简洁和易读。

解决方案

要实现一个装饰器,我们通常会定义一个外部函数,这个外部函数接收一个函数作为参数(也就是我们要装饰的那个函数)。在外部函数内部,我们再定义一个嵌套函数(通常命名为

wrapper
),这个
wrapper
函数才是真正执行额外逻辑的地方,它会调用原始函数,并在调用前后做一些事情。最后,外部函数会返回这个
wrapper
函数。

举个最常见的例子,我们想给一个函数加上日志功能,记录它被调用的时间和参数:

import time
import functools

def log_calls(func):
    """
    一个简单的日志装饰器,记录函数调用。
    """
    @functools.wraps(func) # 这一行很重要,它保留了原函数的元信息
    def wrapper(*args, **kwargs):
        print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 调用函数: {func.__name__},参数: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 函数 {func.__name__} 执行完毕,返回: {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    """计算两个数的和"""
    time.sleep(0.1) # 模拟耗时操作
    return a + b

@log_calls
def greet(name, greeting="Hello"):
    """向指定名字的人打招呼"""
    return f"{greeting}, {name}!"

# 调用被装饰的函数
print(f"结果: {add(10, 20)}")
print(f"结果: {greet('Alice', greeting='Hi')}")

这里,

log_calls
就是我们的装饰器。当我们在
add
函数上方写上
@log_calls
时,Python解释器实际上做了这样的事情:
add = log_calls(add)
。也就是说,
add
这个变量现在指向的不再是原来的
add
函数,而是
log_calls
函数返回的那个
wrapper
函数。当调用
add(10, 20)
时,实际上是调用了
wrapper(10, 20)
wrapper
内部再调用原始的
add
函数。
functools.wraps
的使用是为了让装饰后的函数仍然保持原函数的名称、文档字符串等元信息,这在调试和使用帮助文档时非常有用,不然你看到的函数名就都是
wrapper
了,那可就太让人困惑了。

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

Python装饰器背后的魔法:闭包与函数作为一等公民

在我看来,要真正理解装饰器,就得先搞明白Python里“函数是第一类对象(First-Class Citizen)”这个概念,以及“闭包(Closure)”是什么。这不光是装饰器的基石,也是Python很多高级特性的核心。

函数作为一等公民,意味着函数在Python里和整数、字符串这些数据类型没什么两样。你可以:

  1. 把函数赋值给变量:
    my_func = add
  2. 把函数作为参数传给另一个函数:
    map(str, [1, 2, 3])
  3. 把函数作为另一个函数的返回值:这正是装饰器里外部函数返回
    wrapper
    的关键。
  4. 把函数存储在数据结构里(比如列表或字典)。

而闭包,则是在一个函数内部定义了另一个函数,并且内部函数引用了外部函数的局部变量,当外部函数执行完毕并返回内部函数时,即使外部函数的执行环境已经销毁,内部函数仍然能够“记住”并访问外部函数的那些局部变量。

在我们的

log_calls
例子里:

晓象AI资讯阅读神器
晓象AI资讯阅读神器

晓象-AI时代的资讯阅读神器

下载
  • log_calls
    是外部函数。
  • func
    (也就是被装饰的
    add
    greet
    )是
    log_calls
    的局部变量。
  • wrapper
    是内部函数,它引用了外部函数的局部变量
    func
  • log_calls
    执行完毕并返回
    wrapper
    时,
    wrapper
    就形成了一个闭包,它“捕获”了
    func
    这个变量。所以,即使
    log_calls
    已经执行完了,
    wrapper
    在被调用时依然知道它应该去调用哪个原始函数。

这种机制非常强大,它允许我们在不修改原函数代码的前提下,对其行为进行扩展。这在很多场景下都极其有用,比如权限验证、缓存、性能监控、事务管理等等,都是典型的“横切关注点”,用装饰器来处理简直是天作之合。

如何编写带参数的装饰器?

有时候,我们希望装饰器本身也能接受一些配置参数,比如一个日志装饰器,我们可能想指定日志级别,或者一个权限装饰器,我们想指定需要的角色。这时,我们的装饰器就需要变成一个“装饰器工厂”,也就是说,一个函数,它接收参数,然后返回一个真正的装饰器。

这个模式会多一层嵌套,看起来可能会有点绕,但理解了前面闭包的概念,这也就水到渠成了。

import time
import functools

def timer(unit="seconds"):
    """
    一个带参数的计时装饰器,可以指定时间单位。
    unit: 'seconds', 'milliseconds', 'microseconds'
    """
    def decorator(func): # 这才是真正的装饰器
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            start_time = time.perf_counter()
            result = func(*args, **kwargs)
            end_time = time.perf_counter()
            duration = end_time - start_time

            if unit == "milliseconds":
                duration *= 1000
                unit_str = "ms"
            elif unit == "microseconds":
                duration *= 1_000_000
                unit_str = "μs"
            else:
                unit_str = "s"

            print(f"函数 {func.__name__} 执行耗时: {duration:.4f} {unit_str}")
            return result
        return wrapper
    return decorator

@timer(unit="milliseconds") # 这里传递了参数
def complex_calculation(n):
    """模拟一个复杂的计算"""
    total = 0
    for i in range(n):
        total += i * i
    time.sleep(0.05) # 额外模拟一点IO耗时
    return total

@timer() # 不传参数时,使用默认单位
def simple_task():
    """一个简单的任务"""
    time.sleep(0.02)
    return "Task Done"

print(f"计算结果: {complex_calculation(100000)}")
print(f"任务状态: {simple_task()}")

这里,

timer
函数就是那个“装饰器工厂”。它接收
unit
参数,然后返回
decorator
函数。
decorator
函数才是我们熟悉的那个接收函数作为参数并返回
wrapper
的结构。当写
@timer(unit="milliseconds")
时,Python解释器首先调用
timer("milliseconds")
,这会返回
decorator
函数。然后,这个返回的
decorator
函数再被用来装饰
complex_calculation
,等价于
complex_calculation = decorator(complex_calculation)
。这样,
unit
这个参数就被
decorator
wrapper
形成的闭包“捕获”了,可以在
wrapper
内部使用。

深入探索:类装饰器与更灵活的状态管理

除了函数装饰器,Python还允许我们使用类来作为装饰器。类装饰器在某些场景下,比如需要维护状态、或者需要更复杂的初始化逻辑时,会显得更加直观和强大。

一个类要作为装饰器,最核心的一点是它需要实现

__call__
方法。这样,类的实例就可以像函数一样被调用。当类被用作装饰器时,
@ClassName
实际上是创建了
ClassName
的一个实例,然后用这个实例来替换被装饰的函数。

import time
import functools

class CallCounter:
    """
    一个类装饰器,用于统计函数被调用的次数。
    """
    def __init__(self, func):
        # 初始化时,接收被装饰的函数
        functools.update_wrapper(self, func) # 同样保留原函数元信息
        self.func = func
        self.count = 0 # 维护调用次数的状态

    def __call__(self, *args, **kwargs):
        # 当被装饰的函数被调用时,实际上是调用了__call__方法
        self.count += 1
        print(f"函数 {self.func.__name__} 已被调用 {self.count} 次。")
        return self.func(*args, **kwargs)

@CallCounter
def calculate_sum(a, b):
    """计算和"""
    return a + b

@CallCounter
def say_hello(name):
    """打招呼"""
    return f"Hello, {name}!"

# 调用被装饰的函数
print(calculate_sum(1, 2))
print(calculate_sum(3, 4))
print(say_hello("World"))
print(calculate_sum(5, 6))

这里,

CallCounter
类被用作装饰器。当
@CallCounter
作用于
calculate_sum
时,Python解释器会执行
calculate_sum = CallCounter(calculate_sum)
。这意味着
calculate_sum
现在不再是原来的函数,而是
CallCounter
类的一个实例。当后续调用
calculate_sum(1, 2)
时,实际上是调用了这个实例的
__call__
方法,从而实现了计数和原始函数调用的逻辑。

类装饰器特别适合需要内部状态或者需要在多个被装饰函数之间共享某些配置或资源的场景。比如,一个数据库连接池的装饰器,或者一个复杂的缓存机制,用类来实现可能会让代码结构更清晰,状态管理也更集中。当然,这并不是说函数装饰器就不能实现有状态的,只是类提供了一种更面向对象的封装方式。选择哪种方式,很多时候取决于具体的需求和个人偏好,但了解它们的原理和适用场景,总能帮助我们做出更明智的决策。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
python开发工具
python开发工具

php中文网为大家提供各种python开发工具,好的开发工具,可帮助开发者攻克编程学习中的基础障碍,理解每一行源代码在程序执行时在计算机中的过程。php中文网还为大家带来python相关课程以及相关文章等内容,供大家免费下载使用。

778

2023.06.15

python打包成可执行文件
python打包成可执行文件

本专题为大家带来python打包成可执行文件相关的文章,大家可以免费的下载体验。

686

2023.07.20

python能做什么
python能做什么

python能做的有:可用于开发基于控制台的应用程序、多媒体部分开发、用于开发基于Web的应用程序、使用python处理数据、系统编程等等。本专题为大家提供python相关的各种文章、以及下载和课程。

769

2023.07.25

format在python中的用法
format在python中的用法

Python中的format是一种字符串格式化方法,用于将变量或值插入到字符串中的占位符位置。通过format方法,我们可以动态地构建字符串,使其包含不同值。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

740

2023.07.31

python教程
python教程

Python已成为一门网红语言,即使是在非编程开发者当中,也掀起了一股学习的热潮。本专题为大家带来python教程的相关文章,大家可以免费体验学习。

1445

2023.08.03

python环境变量的配置
python环境变量的配置

Python是一种流行的编程语言,被广泛用于软件开发、数据分析和科学计算等领域。在安装Python之后,我们需要配置环境变量,以便在任何位置都能够访问Python的可执行文件。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

571

2023.08.04

python eval
python eval

eval函数是Python中一个非常强大的函数,它可以将字符串作为Python代码进行执行,实现动态编程的效果。然而,由于其潜在的安全风险和性能问题,需要谨慎使用。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

581

2023.08.04

scratch和python区别
scratch和python区别

scratch和python的区别:1、scratch是一种专为初学者设计的图形化编程语言,python是一种文本编程语言;2、scratch使用的是基于积木的编程语法,python采用更加传统的文本编程语法等等。本专题为大家提供scratch和python相关的文章、下载、课程内容,供大家免费下载体验。

752

2023.08.11

拼多多赚钱的5种方法 拼多多赚钱的5种方法
拼多多赚钱的5种方法 拼多多赚钱的5种方法

在拼多多上赚钱主要可以通过无货源模式一件代发、精细化运营特色店铺、参与官方高流量活动、利用拼团机制社交裂变,以及成为多多进宝推广员这5种方法实现。核心策略在于通过低成本、高效率的供应链管理与营销,利用平台社交电商红利实现盈利。

31

2026.01.26

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.3万人学习

Django 教程
Django 教程

共28课时 | 3.5万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号