Um die Anwendung von Python-Timern besser zu beherrschen, haben wir auch Hintergrundwissen zu Python-Klassen, Kontextmanagern und Dekoratoren hinzugefügt. Aus Platzgründen wird die Verwendung von Kontextmanagern und Dekoratoren zur Optimierung von Python-Timern in nachfolgenden Artikeln untersucht und ist nicht Gegenstand dieses Artikels.
Zuerst fügen wir einem Codestück einen Python-Timer hinzu, um dessen Leistung zu überwachen.
Das integrierte Zeitmodul[1] in Python verfügt über mehrere Funktionen, die Zeit messen können:
perf_counter() Gibt den Wert in Sekunden des Leistungszählers zurück, d. h. der Uhr mit der höchsten verfügbaren Auflösung zur Messung kurzer Dauern.Verwenden Sie zunächst perf_counter(), um einen Python-Timer zu erstellen. Ich werde es mit anderen Python-Timerfunktionen vergleichen, um die Vorteile von perf_counter() zu sehen. BeispielErstellen Sie ein Skript und definieren Sie eine kurze Funktion: Laden Sie einen Datensatz aus der Tsinghua Cloud herunter.
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()
>>> import time >>> time.perf_counter() 394.540232282 >>> time.perf_counter()# 几秒钟后 413.31714087
# 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()
该程序耗时: 0.026 seconds
$ python -m pip install codetiming
# 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")
接下来自定义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()的示例进行比较。代码的结构相似,但现在代码更清晰了,这也是使用类的好处之一。通过仔细选择类、方法和属性名称,可以使你的代码非常具有描述性!
现在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 计时器加入更多便利性和灵活性,那怎么做呢?
首先,自定义用于报告所用时间的文本。在前面的代码中,文本 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,以交互方式使用它时使其更具信息性。下面操作是实例化一个计时器类,并查看其信息:
>>> 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 数据类的一些注意事项:
新 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 计时器有几个好处:
这个类非常灵活,几乎可以在任何需要监控代码运行时间的情况下使用它。但是,在接下来的部分中,云朵君将和大家一起了解如何使用上下文管理器和装饰器,这将更方便地对代码块和函数进行计时。
Das obige ist der detaillierte Inhalt vonBringen Sie Ihnen Schritt für Schritt bei, wie Sie einen Python-Timer implementieren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!