手把手教你實作一個 Python 計時器
為了更好地掌握 Python 計時器的應用,我們後面也補充了有關Python類別、上下文管理器和裝飾器的背景知識。因篇幅限制,其中利用上下文管理器和裝飾器優化 Python 計時器,將在後續文章中學習,不在本篇文章範圍內。
Python 計時器
首先,我們在某段程式碼中加入一個Python 計時器以監控其效能。
Python 定時器函數
Python 中的內建time[1]模組中有幾個可以測量時間的函數:
- monotonic()
- perf_counter()
- process_time()
- time()
Python 3.7 引入了幾個新函數,如thread_time()[2],以及上述所有函數的奈秒版本,以_ns後綴命名。例如,perf_counter_ns()是perf_counter()的奈秒版本的。
perf_counter()
傳回效能計數器的值(以秒為單位),即具有最高可用解析度的時脈以測量短持續時間。
首先,使用perf_counter()建立一個 Python 計時器。將把它與其他 Python 計時器函數進行比較,看看 perf_counter() 的優勢。
範例
建立一個腳本,定義一個簡短的函數:從清華雲端下載一組資料。
import requests def main(): source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} res = requests.get(source_url, headers=headers) with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) if __name__=="__main__": main()
我們可以使用 Python 計時器來監控該腳本的效能。
第一個 Python 計時器
現在使用函數time.perf_counter()函數建立計時器,這是一個非常適合針對部分程式碼的效能計時的計數器。
perf_counter()從某個未指定的時刻開始測量時間(以秒為單位),這表示對該函數的單一呼叫的傳回值沒有用。但當查看對perf_counter()兩次呼叫之間的差異時,可以計算出兩次呼叫之間經過了多少秒。
>>> import time >>> time.perf_counter() 394.540232282 >>> time.perf_counter()# 几秒钟后 413.31714087
在此範例中,兩次呼叫 perf_counter() 相隔近 19 秒。可以透過計算兩個輸出之間的差異來確認這一點:413.31714087 - 394.540232282 = 18.78。
現在可以將 Python 計時器加入到範例程式碼中:
# download_data.py import requests import time def main(): tic = time.perf_counter() source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} res = requests.get(source_url, headers=headers) with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) toc = time.perf_counter() print(f"该程序耗时: {toc - tic:0.4f} seconds") if __name__=="__main__": main()
注意perf_counter()透過計算兩次呼叫之間的差異來列印整個程式運行所花費的時間。
print()函數中 f 字串前面的表示這是一個 f-string ,這是格式化文字字串的較為便捷的方式。 :0.4f是一個格式說明符,表示數字,toc - tic應列印為帶有四位小數的十進位數。
執行程式可以看到程式經過的時間:
该程序耗时: 0.026 seconds
就是這麼簡單。接下來我們一起學習如何將 Python 計時器包裝到一個類別、一個上下文管理器和一個裝飾器中(該系列後續兩篇文章,待更新),這樣可以更加一致和方便使用計時器。
一個 Python 定時器類別
這裡我們至少需要一個變數來儲存 Python 計時器的狀態。接下來我們建立一個與手動呼叫 perf_counter() 相同的類,但更具可讀性和一致性。
建立和更新Timer類,使用該類別以多種不同方式對程式碼進行計時。
$ python -m pip install codetiming
瞭解 Python 中的類別
Class類別是物件導向程式設計的主要建構塊。類別本質上是一個模板,可以使用它來創建物件。
在 Python 中,當需要對需要追蹤特定狀態的事物進行建模時,類別非常有用。一般來說,類別是屬性的集合,稱為屬性,以及行為,稱為方法。
建立 Python 計時器類別
類別有利於追蹤狀態。在Timer類別中,想要追蹤計時器何時開始以及已經多少時間。對於Timer類別的第一個實現,將會新增一個._start_time屬性以及.start()和.stop()方法。將以下程式碼加入名為 timer.py 的檔案中:
# timer.py import time class TimerError(Exception): """一个自定义异常,用于报告使用Timer类时的错误""" class Timer: def __init__(self): self._start_time = None def start(self): """Start a new timer""" if self._start_time is not None: raise TimerError(f"Timer is running. Use .stop() to stop it") self._start_time = time.perf_counter() def stop(self): """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") elapsed_time = time.perf_counter() - self._start_time self._start_time = None print(f"Elapsed time: {elapsed_time:0.4f} seconds")
這裡我們需要花點時間仔細瀏覽程式碼,會發現一些不同的事情。
首先定義了一個TimerError Python 類別。此(Exception)符號表示TimerError 繼承自另一個名為Exception的父類別。使用這個內建類別進行錯誤處理。不需要在TimerError中新增任何屬性或方法,但自訂錯誤可以更靈活地處理Timer內部問題。
接下来自定义Timer类。当从一个类创建或实例化一个对象时,代码会调用特殊方法.__init__()初始化实例。在这里定义的第一个Timer版本中,只需初始化._start_time属性,将用它来跟踪 Python 计时器的状态,计时器未运行时它的值为None。计时器运行后,用它来跟踪计时器的启动时间。
注意: ._start_time的第一个下划线(_)前缀是Python约定。它表示._start_time是Timer类的用户不应该操作的内部属性。
当调用.start()启动新的 Python 计时器时,首先检查计时器是否运行。然后将perf_counter()的当前值存储在._start_time中。
另一方面,当调用.stop()时,首先检查Python计时器是否正在运行。如果是,则将运行时间计算为perf_counter()的当前值与存储在._start_time中的值的差值。最后,重置._start_time,以便重新启动计时器,并打印运行时间。
以下是使用Timer方法:
from timer import Timer t = Timer() t.start() # 几秒钟后 t.stop()
Elapsed time: 3.8191 seconds
将此示例与前面直接使用perf_counter()的示例进行比较。代码的结构相似,但现在代码更清晰了,这也是使用类的好处之一。通过仔细选择类、方法和属性名称,可以使你的代码非常具有描述性!
使用 Python 计时器类
现在Timer类中写入download_data.py。只需要对以前的代码进行一些更改:
# download_data.py import requests from timer import Timer def main(): t = Timer() t.start() source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} res = requests.get(source_url, headers=headers) with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) t.stop() if __name__=="__main__": main()
注意,该代码与之前使用的代码非常相似。除了使代码更具可读性之外,Timer还负责将经过的时间打印到控制台,使得所用时间的记录更加一致。运行代码时,得到的输出几乎相同:
Elapsed time: 0.502 seconds ...
打印经过的时间Timer可能是一致的,但这种方法好像不是很灵活。下面我们添加一些更加灵活的东西到代码中。
增加更多的便利性和灵活性
到目前为止,我们已经了解到类适用于我们想要封装状态并确保代码一致性的情况。在本节中,我们将一起给 Python 计时器加入更多便利性和灵活性,那怎么做呢?
- 在报告消耗的时间时,使用可调整的文本和格式
- 将日志记录打印到控制台、写入到日志文件或程序的其他部分
- 创建一个可以在多次调用中可积累的Python计时器
- 构建 Python 计时器的信息表示
首先,自定义用于报告所用时间的文本。在前面的代码中,文本 f"Elapsed time: {elapsed_time:0.4f} seconds" 被生硬编码到 .stop() 中。如若想使得类代码更加灵活, 可以使用实例变量,其值通常作为参数传递给.__init__()并存储到 self 属性。为方便起见,我们还可以提供合理的默认值。
要添加.text为Timer实例变量,可执行以下操作timer.py:
# timer.py def __init__(self, text="Elapsed time: {:0.4f} seconds"): self._start_time = None self.text = text
注意,默认文本"Elapsed time: {:0.4f} seconds"是作为一个常规字符串给出的,而不是f-string。这里不能使用f-string,因为f-string会立即计算,当你实例化Timer时,你的代码还没有计算出消耗的时间。
注意: 如果要使用f-string来指定.text,则需要使用双花括号来转义实际经过时间将替换的花括号。
如:f"Finished {task} in {{:0.4f}} seconds"。如果task的值是"reading",那么这个f-string将被计算为"Finished reading in {:0.4f} seconds"。
在.stop()中,.text用作模板并使用.format()方法填充模板:
# timer.py def stop(self): """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") elapsed_time = time.perf_counter() - self._start_time self._start_time = None print(self.text.format(elapsed_time))
在此更新为timer.py之后,可以将文本更改如下:
from timer import Timer t = Timer(text="You waited {:.1f} seconds") t.start() # 几秒钟后 t.stop()
You waited 4.1 seconds
接下来,我们不只是想将消息打印到控制台,还想保存时间测量结果,这样可以便于将它们存储在数据库中。可以通过从.stop()返回elapsed_time的值来实现这一点。然后,调用代码可以选择忽略该返回值或保存它以供以后处理。
如果想要将Timer集成到日志logging中。要支持计时器的日志记录或其他输出,需要更改对print()的调用,以便用户可以提供自己的日志记录函数。这可以用类似于你之前定制的文本来完成:
# timer.py # ... class Timer: def __init__( self, text="Elapsed time: {:0.4f} seconds", logger=print ): self._start_time = None self.text = text self.logger = logger # 其他方法保持不变 def stop(self): """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") elapsed_time = time.perf_counter() - self._start_time self._start_time = None if self.logger: self.logger(self.text.format(elapsed_time)) return elapsed_time
不是直接使用print(),而是创建另一个实例变量 self.logger,引用一个接受字符串作为参数的函数。除此之外,还可以对文件对象使用logging.info()或.write()等函数。还要注意if中,它允许通过传递logger=None来完全关闭打印。
以下是两个示例,展示了新功能的实际应用:
from timer import Timer import logging t = Timer(logger=logging.warning) t.start() # 几秒钟后 t.stop()# A few seconds later
WARNING:root:Elapsed time: 3.1610 seconds 3.1609658249999484
t = Timer(logger=None) t.start() # 几秒钟后 value = t.stop() value
4.710851433001153
接下来第三个改进是积累时间度量的能力。例如,在循环中调用一个慢速函数时,希望以命名计时器的形式添加更多的功能,并使用一个字典来跟踪代码中的每个Python计时器。
我们扩展download_data.py脚本。
# download_data.py import requests from timer import Timer def main(): t = Timer() t.start() source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} for i in range(10): res = requests.get(source_url, headers=headers) with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) t.stop() if __name__=="__main__": main()
这段代码的一个微妙问题是,不仅要测量下载数据所需的时间,还要测量 Python 存储数据到磁盘所花费的时间。这可能并重要,有时候这两者所花费的时间可以忽略不计。但还是希望有一种方法可以精确地计时没一个步骤,将会更好。
有几种方法可以在不改变Timer当前实现的情况下解决这个问题,且只需要几行代码即可实现。
首先,将引入一个名为.timers的字典作为Timer的类变量,此时Timer的所有实例将共享它。通过在任何方法之外定义它来实现它:
class Timer: timers = {}
类变量可以直接在类上访问,也可以通过类的实例访问:
>>> from timer import Timer >>> Timer.timers {} >>> t = Timer() >>> t.timers {} >>> Timer.timers is t.timers True
在这两种情况下,代码都返回相同的空类字典。
接下来向 Python 计时器添加可选名称。可以将该名称用于两种不同的目的:
- 在代码中查找经过的时间
- 累加同名定时器
要向Python计时器添加名称,需要对 timer.py 进行更改。首先,Timer 接受 name 参数。第二,当计时器停止时,运行时间应该添加到 .timers 中:
# timer.py # ... class Timer: timers = {} def __init__( self, name=None, text="Elapsed time: {:0.4f} seconds", logger=print, ): self._start_time = None self.name = name self.text = text self.logger = logger # 向计时器字典中添加新的命名计时器 if name: self.timers.setdefault(name, 0) # 其他方法保持不变 def stop(self): """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") elapsed_time = time.perf_counter() - self._start_time self._start_time = None if self.logger: self.logger(self.text.format(elapsed_time)) if self.name: self.timers[self.name] += elapsed_time return elapsed_time
注意,在向.timers中添加新的Python计时器时,使用了.setdefault()方法。它只在没有在字典中定义name的情况下设置值,如果name已经在.timers中使用,那么该值将保持不变,此时可以积累几个计时器:
>>> from timer import Timer >>> t = Timer("accumulate") >>> t.start() >>> t.stop()# A few seconds later Elapsed time: 3.7036 seconds 3.703554293999332 >>> t.start() >>> t.stop()# A few seconds later Elapsed time: 2.3449 seconds 2.3448921170001995 >>> Timer.timers {'accumulate': 6.0484464109995315}
现在可以重新访问download_data.py并确保仅测量下载数据所花费的时间:
# download_data.py import requests from timer import Timer def main(): t = Timer("download", logger=None) source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} for i in range(10): t.start() res = requests.get(source_url, headers=headers) t.stop() with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) download_time = Timer.timers["download"] print(f"Downloaded 10 dataset in {download_time:0.2f} seconds") if __name__=="__main__": main()
现在你有了一个非常简洁的版本,Timer它一致、灵活、方便且信息丰富!也可以将本节中所做的许多改进应用于项目中的其他类型的类。
Timer改进
最后一个改进Timer,以交互方式使用它时使其更具信息性。下面操作是实例化一个计时器类,并查看其信息:
>>> from timer import Timer >>> t = Timer() >>> t <timer.Timer object at 0x7f0578804320>
最后一行是 Python 表示对象的默认方式。我们从这个结果中看到的信息,并不是很明确,我们接下来对其进行改进。
这里介绍一个 dataclasses 类,该类仅包含在 Python 3.7 及更高版本中。
pip install dataclasses
可以使用@dataclass装饰器将 Python 计时器转换为数据类
# timer.py import time from dataclasses import dataclass, field from typing import Any, ClassVar # ... @dataclass class Timer: timers: ClassVar = {} name: Any = None text: Any = "Elapsed time: {:0.4f} seconds" logger: Any = print _start_time: Any = field(default=None, init=False, repr=False) def __post_init__(self): """Initialization: add timer to dict of timers""" if self.name: self.timers.setdefault(self.name, 0) # 其余代码不变
此代码替换了之前的 .__init__() 方法。请注意数据类如何使用类似于之前看到的用于定义所有变量的类变量语法的语法。事实上,.__init__()是根据类定义中的注释变量自动为数据类创建的。
如果需要注释变量以使用数据类。可以使用此注解向代码添加类型提示。如果不想使用类型提示,那么可以使用 Any 来注释所有变量。接下来我们很快就会学习如何将实际类型提示添加到我们的数据类中。
以下是有关 Timer 数据类的一些注意事项:
- 第 6 行:@dataclass 装饰器将Timer 定义为数据类。
- 第 8 行:数据类需要特殊的 ClassVar 注释来指定.timers 是一个类变量。
- 第 9 到 11 行:.name、.text 和.logger 将被定义为 Timer 上的属性,可以在创建 Timer 实例时指定其值。它们都有给定的默认值。
- 第 12 行:回想一下._start_time 是一个特殊属性,用于跟踪 Python 计时器的状态,但它应该对用户隐藏。使用dataclasses.field(), ._start_time 应该从.__init__() 和 Timer 的表示中删除。
- 除了设置实例属性之外,可以使用特殊的 .__post_init__() 方法进行初始化。这里使用它将命名的计时器添加到 .timers。
新 Timer 数据类与之前的常规类使用功能一样,但它现在有一个很好的信息表示:
from timer import Timer t = Timer() t
Timer(name=None, text='Elapsed time: {:0.4f} seconds', logger=<built-in function print>)
t.start() # 几秒钟后 t.stop()
Elapsed time: 6.7197 seconds 6.719705373998295
总结
现在我们有了一个非常简洁的 Timer 版本,它一致、灵活、方便且信息丰富!我们还可以将本文中所做的许多改进应用于项目中的其他类型的类。
现在我们访问当前的完整源代码Timer。会注意到在代码中添加了类型提示以获取额外的文档:
# timer.py from dataclasses import dataclass, field import time from typing import Callable, ClassVar, Dict, Optional class TimerError(Exception): """A custom exception used to report errors in use of Timer class""" @dataclass class Timer: timers: ClassVar[Dict[str, float]] = {} name: Optional[str] = None text: str = "Elapsed time: {:0.4f} seconds" logger: Optional[Callable[[str], None]] = print _start_time: Optional[float] = field(default=None, init=False, repr=False) def __post_init__(self) -> None: """Add timer to dict of timers after initialization""" if self.name is not None: self.timers.setdefault(self.name, 0) def start(self) -> None: """Start a new timer""" if self._start_time is not None: raise TimerError(f"Timer is running. Use .stop() to stop it") self._start_time = time.perf_counter() def stop(self) -> float: """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") # Calculate elapsed time elapsed_time = time.perf_counter() - self._start_time self._start_time = None # Report elapsed time if self.logger: self.logger(self.text.format(elapsed_time)) if self.name: self.timers[self.name] += elapsed_time return elapsed_time
总结下: 使用类创建 Python 计时器有几个好处:
- 可读性:仔细选择类和方法名称,你的代码将更自然地阅读。
- 一致性:将属性和行为封装到属性和方法中,你的代码将更易于使用。
- 灵活性:使用具有默认值而不是硬编码值的属性,你的代码将是可重用的。
这个类非常灵活,几乎可以在任何需要监控代码运行时间的情况下使用它。但是,在接下来的部分中,云朵君将和大家一起了解如何使用上下文管理器和装饰器,这将更方便地对代码块和函数进行计时。
以上是手把手教你實作一個 Python 計時器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

在 Sublime Text 中運行 Python 代碼,需先安裝 Python 插件,再創建 .py 文件並編寫代碼,最後按 Ctrl B 運行代碼,輸出會在控制台中顯示。

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

PHP的核心優勢包括易於學習、強大的web開發支持、豐富的庫和框架、高性能和可擴展性、跨平台兼容性以及成本效益高。 1)易於學習和使用,適合初學者;2)與web服務器集成好,支持多種數據庫;3)擁有如Laravel等強大框架;4)通過優化可實現高性能;5)支持多種操作系統;6)開源,降低開發成本。

MySQL与其他编程语言相比,主要用于存储和管理数据,而其他语言如Python、Java、C 则用于逻辑处理和应用开发。MySQL以其高性能、可扩展性和跨平台支持著称,适合数据管理需求,而其他语言在各自领域如数据分析、企业应用和系统编程中各有优势。

Golang在性能和可擴展性方面優於Python。 1)Golang的編譯型特性和高效並發模型使其在高並發場景下表現出色。 2)Python作為解釋型語言,執行速度較慢,但通過工具如Cython可優化性能。
