0

0

Python函数怎样用装饰器实现函数执行时间统计 Python函数计时装饰器的入门编写方法​

爱谁谁

爱谁谁

发布时间:2025-08-13 10:45:02

|

590人浏览过

|

来源于php中文网

原创

使用装饰器计时无需修改函数内部代码,通过在调用前后记录时间差来统计执行耗时;2. 核心实现是利用time.perf_counter()获取高精度时间,结合functools.wraps保留原函数元信息;3. 装饰器的优势在于解耦和复用,避免在多个函数中重复插入计时代码;4. 可扩展为带参数的装饰器,支持自定义日志级别、输出格式等;5. 注意事项包括装饰器自身开销、i/o等待时间影响、递归函数的重复计时问题以及异步函数需使用async装饰器。该方法在不侵入业务逻辑的前提下实现高效性能监控,适用于大多数常规场景的执行时间分析。

Python函数怎样用装饰器实现函数执行时间统计 Python函数计时装饰器的入门编写方法​

说白了,Python里用装饰器来统计函数执行时间,就是给函数套个“壳”,这个“壳”负责在函数跑之前记个时,跑完之后再记个时,然后一减,时间就出来了。核心思想就是不改动函数本身的代码,而是通过一个外部机制来增强它,让你的核心业务逻辑保持干净利落。这对于性能分析、调试,或者仅仅是想知道某个操作到底耗了多久,都非常方便。

解决方案

要实现一个函数计时装饰器,我们通常会用到Python内置的

time
模块。其中,
time.perf_counter()
是个不错的选择,因为它能提供最高精度的计时,非常适合测量短时间的程序执行。

下面是一个基础的计时装饰器实现:

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

import time
import functools # 用于保留被装饰函数的元数据

def timer(func):
    """
    一个简单的计时装饰器,用于测量函数执行时间。
    """
    @functools.wraps(func) # 这一行很重要,它能保留原函数的__name__, __doc__等属性
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter() # 记录开始时间
        result = func(*args, **kwargs)   # 执行被装饰的函数
        end_time = time.perf_counter()   # 记录结束时间
        duration = end_time - start_time # 计算执行时长

        print(f"函数 '{func.__name__}' 执行耗时: {duration:.4f} 秒")
        return result # 返回被装饰函数的执行结果
    return wrapper

# 如何使用这个装饰器
@timer
def example_function(n):
    """一个模拟耗时操作的函数"""
    print(f"开始执行 example_function({n})...")
    time.sleep(n) # 模拟耗时操作
    print(f"example_function({n}) 执行完毕。")
    return f"完成了 {n} 秒的等待"

@timer
def another_task(a, b):
    """另一个简单的函数"""
    print(f"正在计算 {a} + {b}...")
    total = sum(range(a * b)) # 一个稍微耗时的计算
    return total

if __name__ == "__main__":
    print("--- 第一次调用 ---")
    example_function(1) # 调用被装饰的函数

    print("\n--- 第二次调用 ---")
    example_function(0.5)

    print("\n--- 第三次调用 ---")
    result = another_task(1000, 2000)
    print(f"another_task 的结果是: {result}")

这段代码里,

timer
函数就是我们的装饰器。它接收一个函数
func
作为参数,然后定义了一个内部函数
wrapper
wrapper
才是真正执行计时逻辑的地方:它在调用
func
前后记录时间,并打印出时长。最后,
timer
返回这个
wrapper
函数。
@functools.wraps(func)
这一行非常关键,它能确保被装饰后的
wrapper
函数仍然拥有原函数
func
的名称、文档字符串等元信息,这对于调试和代码自省来说非常重要。

为什么我们要用装饰器来计时,而不是直接在函数内部加代码?

说实话,刚开始学编程的时候,我也会想,直接在函数开头和结尾各加一行计时代码不就得了?比如这样:

import time

def my_old_function():
    start = time.perf_counter()
    # 核心业务逻辑
    time.sleep(1)
    end = time.perf_counter()
    print(f"耗时: {end - start} 秒")

这当然能工作,但想想看,如果你的项目里有几十上百个函数都需要计时呢?你得把这几行代码复制粘贴几十上百次。这简直是维护者的噩梦!

我觉得,用装饰器来做计时,最大的好处就是“解耦”和“复用”。

PaperFake
PaperFake

AI写论文

下载
  • 解耦(Separation of Concerns):你的函数就应该只专注于它自己的核心业务逻辑。计时、日志、权限验证这些“横切关注点”,就不应该混在函数的主体里。装饰器就像一个独立的插件,把这些辅助功能从核心逻辑中抽离出来,让你的代码看起来更清晰,更容易理解。
  • 复用性(Reusability):你只需要写一次计时逻辑,然后通过
    @timer
    这种优雅的语法,就能轻松地应用到任何你需要计时的函数上。如果哪天你想改变计时的精度,或者想把计时结果记录到文件而不是打印到控制台,你只需要修改
    timer
    装饰器本身,所有被它装饰的函数都会自动更新,根本不需要动那些函数的代码。这在我看来,是代码组织和维护效率上质的飞跃。

这种方式,在我实际的项目经验里,真的是屡试不爽。它让我在不修改原有业务代码的前提下,轻松地添加或移除各种辅助功能,大大提升了开发效率和代码质量。

除了基础计时,这个装饰器还能怎么玩?

既然我们已经掌握了基础,那自然会想,这玩意儿还能玩出什么花样来?计时装饰器远不止打印个时间那么简单,它有很多扩展的可能性,能让你的程序分析能力更上一层楼。

  1. 带参数的计时装饰器: 有时候你可能想控制计时结果的输出格式,或者决定是否要打印结果。这时候,你可以让装饰器本身接收参数。这需要多一层函数嵌套:

    import time
    import functools
    import logging # 引入日志模块
    
    # 配置一个简单的日志器
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
    
    def configurable_timer(log_level=logging.INFO, message_prefix=""):
        """
        一个可配置的计时装饰器,可以指定日志级别和消息前缀。
        """
        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
    
                log_message = f"{message_prefix}函数 '{func.__name__}' 执行耗时: {duration:.4f} 秒"
                logging.log(log_level, log_message) # 使用日志模块输出
    
                return result
            return wrapper
        return decorator
    
    @configurable_timer(log_level=logging.DEBUG, message_prefix="[DEBUG_TIMING] ")
    def debug_task():
        time.sleep(0.1)
        print("这是一个调试任务。")
    
    @configurable_timer(log_level=logging.WARNING, message_prefix="[PERFORMANCE_WARNING] ")
    def critical_task():
        time.sleep(0.8)
        print("这是一个关键任务,可能耗时过长。")
    
    if __name__ == "__main__":
        print("\n--- 可配置计时器示例 ---")
        debug_task()
        critical_task()

    这样,你就可以根据需要调整装饰器的行为,比如在开发环境打印详细的DEBUG信息,而在生产环境只记录WARNING级别的性能告警。

  2. 记录到文件或数据库: 如果你的系统需要长期监控性能,仅仅打印到控制台肯定不够。你可以把计时结果写入日志文件,或者存储到数据库中,方便后续的数据分析和可视化。这只需要在

    wrapper
    函数内部,把
    print
    语句替换成文件写入或数据库操作即可。

  3. 聚合统计: 对于频繁调用的函数,你可能不希望每次都打印一行日志,而是希望在程序运行结束后,能看到这个函数总共被调用了多少次,平均每次耗时多少,最大耗时多少等等。这可以通过在装饰器外部维护一个字典或列表来存储每次调用的数据,然后在程序退出时统一处理。

这些扩展思路,让简单的计时装饰器变得异常强大,能适应各种复杂的性能监控需求。

在实际项目中,使用计时装饰器有哪些常见的“坑”或者需要注意的地方?

任何工具用起来,总会有些需要留心的小细节,计时装饰器也不例外。在我用它的过程中,也遇到过一些让我挠头的问题,总结下来,主要有这么几点:

  1. 装饰器本身的开销: 虽然

    time.perf_counter()
    非常高效,但装饰器本身的代码(函数调用、时间戳获取、减法运算、打印或日志操作)也是有开销的。对于那些执行时间极短(比如微秒级别)的函数,装饰器自身的开销可能会占到总耗时的很大一部分,甚至比函数本身执行的时间还要长。这时候,计时结果可能就不那么准确了。如果你要测量的是纳秒级的操作,可能需要更底层的分析工具。但对于大多数日常业务逻辑,这种开销通常可以忽略不计。

  2. I/O密集型操作的“误导性”:

    time.perf_counter()
    测量的是“墙上时钟时间”(wall-clock time),也就是从开始到结束实际流逝的时间。这意味着,如果你的函数里有大量的网络请求、文件读写或者数据库查询(这些都是I/O密集型操作),那么计时器会把等待这些I/O操作完成的时间也算进去。这并不是CPU实际执行代码的时间。 举个例子,一个函数可能大部分时间都在等待网络响应,而不是在CPU上进行计算。这种情况下,计时器告诉你函数运行了5秒,但可能CPU只工作了10毫秒。这并不能说明你的CPU计算逻辑慢,而是I/O操作慢。如果你想区分CPU时间和等待时间,可能需要结合
    time.process_time()
    (测量CPU时间)或者更专业的性能分析工具。

  3. 递归函数的计时: 如果你直接把计时装饰器应用到一个递归函数上,比如计算斐波那契数列的递归函数:

    @timer
    def fibonacci(n):
        if n <= 1:
            return n
        return fibonacci(n-1) + fibonacci(n-2)

    你会发现,每次递归调用都会触发计时器的打印。这会导致输出信息爆炸,而且你可能只关心最外层那次

    fibonacci(n)
    的总耗时,而不是每次内部递归调用的耗时。解决这个问题,通常需要调整装饰器的逻辑,比如只在最外层调用时才记录和打印时间,或者使用一个全局计数器来判断是否是顶层调用。

  4. 异步函数(

    async def
    )的特殊处理: 在现代Python中,异步编程(
    asyncio
    )越来越常见。如果你尝试用上面给出的同步计时装饰器去装饰一个
    async def
    函数,你会发现它会报错或者行为异常。这是因为异步函数需要
    await
    关键字来暂停和恢复执行,而同步装饰器无法处理这种上下文切换。 对于异步函数,你的装饰器也需要是异步的:

    import time
    import functools
    import asyncio
    
    def async_timer(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs): # 注意这里是 async def
            start_time = time.perf_counter()
            result = await func(*args, **kwargs) # 注意这里是 await
            end_time = time.perf_counter()
            duration = end_time - start_time
            print(f"异步函数 '{func.__name__}' 执行耗时: {duration:.4f} 秒")
            return result
        return wrapper
    
    @async_timer
    async def async_example():
        print("开始异步任务...")
        await asyncio.sleep(0.2) # 模拟异步I/O等待
        print("异步任务完成。")
    
    if __name__ == "__main__":
        print("\n--- 异步计时器示例 ---")
        asyncio.run(async_example())

    记住,同步装饰器不能直接用于异步函数,反之亦然。这在我第一次碰到的时候,着实花了一点时间才反应过来。

这些“坑”和注意事项,并不是说装饰器不好用,而是任何工具都有其适用场景和局限性。了解它们,能让你在实际项目中更准确、更高效地利用计时装饰器进行性能分析。

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

更多
python中print函数的用法
python中print函数的用法

python中print函数的语法是“print(value1, value2, ..., sep=' ', end=' ', file=sys.stdout, flush=False)”。本专题为大家提供print相关的文章、下载、课程内容,供大家免费下载体验。

193

2023.09.27

python print用法与作用
python print用法与作用

本专题整合了python print的用法、作用、函数功能相关内容,阅读专题下面的文章了解更多详细教程。

19

2026.02.03

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

760

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

221

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1568

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

651

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

1228

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

1204

2024.04.29

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

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

精品课程

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

共4课时 | 22.5万人学习

Django 教程
Django 教程

共28课时 | 5万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.9万人学习

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

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