>  기사  >  백엔드 개발  >  Python 타이머를 구현하는 방법을 단계별로 가르쳐주세요.

Python 타이머를 구현하는 방법을 단계별로 가르쳐주세요.

王林
王林앞으로
2023-04-14 16:04:032269검색

Python 타이머의 적용을 더 잘 익히기 위해 Python 클래스, 컨텍스트 관리자 및 데코레이터에 대한 배경 지식도 추가했습니다. 공간 제한으로 인해 Python 타이머를 최적화하기 위해 컨텍스트 관리자와 데코레이터를 사용하는 방법은 후속 기사에서 연구할 예정이며 이 기사의 범위에 포함되지 않습니다.

Python 타이머를 구현하는 방법을 단계별로 가르쳐주세요.

Python Timer

먼저 코드에 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 타이머를 만듭니다. perf_counter()의 장점을 보기 위해 다른 Python 타이머 함수와 비교해 보겠습니다.

스크립트를 생성하고 짧은 함수를 정의합니다. Tsinghua Cloud에서 데이터 세트를 다운로드합니다.

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-문자열임을 나타내며, 이는 텍스트 문자열의 형식을 지정하는 더 편리한 방법입니다. :0.4f​는 toc - tic이 소수점 네 자리의 십진수로 인쇄되어야 하는 숫자를 나타내는 형식 지정자입니다.

경과 시간을 보려면 프로그램을 실행하세요.

该程序耗时: 0.026 seconds

정말 간단합니다. 다음으로 타이머를 보다 일관되고 편리하게 사용할 수 있도록 Python 타이머를 클래스, 컨텍스트 관리자 및 데코레이터(이 시리즈의 후속 기사 2개, 업데이트 예정)로 래핑하는 방법을 알아 보겠습니다.

Python 타이머 클래스

여기서 Python 타이머의 상태를 저장하려면 최소한 하나의 변수가 필요합니다. 다음으로 perf_counter()를 수동으로 호출하는 것과 동일하지만 더 읽기 쉽고 일관성이 있는 클래스를 만듭니다.

Timer 클래스를 생성 및 업데이트하고 이를 사용하여 다양한 방법으로 코드 시간을 측정합니다.

$ python -m pip install codetiming

Python의 클래스 이해​

클래스​ 클래스는 객체 지향 프로그래밍의 주요 구성 요소입니다. 클래스는 본질적으로 객체를 생성하는 데 사용할 수 있는 템플릿입니다.

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 클래스를 정의합니다. (예외) 기호는 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 计时器添加可选名称。可以将该名称用于两种不同的目的:

  1. 在代码中查找经过的时间
  2. 累加同名定时器

要向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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 51cto.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제