이것은 Python 타이머를 구현하는 방법을 단계별로 가르치는 세 번째 기사입니다. 처음 두 기사: Python 타이머를 구현하는 방법을 단계별로 설명하고 컨텍스트 관리자를 사용하여 Python 타이머를 확장하여 Timer 클래스를 사용하기 편리하고 아름답고 실용적으로 만듭니다.
하지만 우리는 이것에 만족하지 않고, 이를 더욱 단순화할 수 있는 사용 사례가 여전히 있습니다. 코드 베이스에서 특정 함수에 소요된 시간을 추적해야 한다고 가정해 보겠습니다. 컨텍스트 관리자를 사용하면 기본적으로 두 가지 옵션이 있습니다.
with Timer("some_name"): do_something()
py 파일에서 do_something() 함수를 여러 번 호출하면 상황이 매우 달라집니다. 번거롭고 유지 관리가 어렵습니다.
def do_something(): with Timer("some_name"): ...
Timer는 한 곳에만 추가하면 되지만 이렇게 하면 do_something()의 전체 정의에 들여쓰기 수준이 추가됩니다.
더 나은 해결책은 Timer를 데코레이터로 사용하는 것입니다. 데코레이터는 함수와 클래스의 동작을 수정하는 데 사용되는 강력한 구성 요소입니다.
데코레이터는 동작을 수정하기 위해 다른 함수를 래핑하는 함수입니다. 질문이 있을 수 있습니다. 이를 달성하는 방법은 무엇입니까? 실제로 함수는 Python의 일급 객체입니다. 즉, 함수는 다른 일반 객체와 마찬가지로 변수 형태로 다른 함수에 인수로 전달될 수 있습니다. 따라서 여기에는 많은 유연성이 있으며 Python의 가장 강력한 기능 중 일부의 기초가 됩니다.
아무 일도 하지 않는 데코레이터인 첫 번째 예제를 만드는 것부터 시작합니다.
def turn_off(func): return lambda *args, **kwargs: None
먼저 이 Turn_off()는 단지 일반 함수라는 점에 유의하세요. 함수를 유일한 인수로 취하고 다른 함수를 반환하기 때문에 데코레이터입니다. Turn_off()를 사용하여 다른 함수를 수정할 수 있습니다. 예를 들면 다음과 같습니다.
>>> print("Hello") Hello >>> print = turn_off(print) >>> print("Hush") >>> # Nothing is printed
코드 줄 print = Turn_off(print)는 print 문을 장식하기 위해 Turn_off() 데코레이터를 사용합니다. 실제로는 print() 함수를 익명 함수 Lambda *args, **kwargs: None으로 대체하고 Turn_off()를 반환합니다. 익명 함수 람다는 아무것도 하지 않고 None을 반환합니다.
더 풍부한 데코레이터를 정의하려면 내부 기능을 이해해야 합니다. 내부 함수는 다른 함수 내에 정의된 함수입니다. 이 함수의 일반적인 용도 중 하나는 함수 팩토리를 만드는 것입니다.
def create_multiplier(factor): def multiplier(num): return factor * num return multiplier
multiplier()는 create_multiplier() 내부에 정의된 내부 함수입니다. 인수는 multiplier() 내부에서 액세스할 수 있지만 multiplier()는 create_multiplier() 외부에서 정의되지 않습니다.
multiplier
Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'multiplier' is not defined
대신 create_multiplier()를 사용하여 각각 다른 매개변수 인수를 기반으로 하는 새로운 승수 함수를 생성할 수 있습니다.
double = create_multiplier(factor=2) double(3)
6
quadruple = create_multiplier(factor=4) quadruple(7)
28
마찬가지로 내부 함수를 사용하여 데코레이터를 만들 수 있습니다. 데코레이터는 함수를 반환하는 함수입니다.
def triple(func): def wrapper_triple(*args, **kwargs): print(f"Tripled {func.__name__!r}") value = func(*args, **kwargs) return value * 3 return wrapper_triple
triple()은 func() 함수를 유일한 인수로 기대하고 다른 함수 Wrapper_triple()를 반환하는 함수이기 때문에 데코레이터입니다. Triple() 자체의 구조에 주목하세요.
이것은 데코레이터를 정의하는 일반적인 패턴입니다(내부 함수 부분 참고).
다음 코드에서 knock()은 Penny라는 단어를 반환하는 함수입니다. 이를 Triple() 함수에 전달하고 출력이 무엇인지 확인하세요.
>>> def knock(): ... return "Penny! " >>> knock = triple(knock) >>> result = knock() Tripled 'knock' >>> result 'Penny! Penny! Penny! '
우리 모두는 텍스트 문자열에 숫자를 곱하는 것이 문자열의 반복 형태라는 것을 알고 있으므로 'Penny'라는 문자열이 3번 반복됩니다. 노크 = 트리플(노크)일 때 장식이 발생한다고 생각하면 된다.
위 방법은 데코레이터의 기능을 구현하기는 하지만 조금은 서툴러 보입니다. PEP 318은 데코레이터 적용을 위한 보다 편리한 구문을 도입했습니다. 아래의 knock() 정의는 위의 정의와 동일하지만 데코레이터 사용법이 다릅니다.
>>> @triple ... def knock(): ... return "Penny! " ... >>> result = knock() Tripled 'knock' >>> result 'Penny! Penny! Penny! '
@ 기호는 데코레이터를 적용하는 데 사용되며, @triple은 바로 뒤에 정의된 함수에 Triple()이 적용된다는 의미입니다.
Python 标准库中定义的装饰器方法之一是:@functools.wraps。这在定义你自己的装饰器时非常有用。前面说过,装饰器是用另一个函数替换了一个函数,会给你的函数带来一个微妙的变化:
knock
<function triple.<locals>.wrapper_triple at 0x7fa3bfe5dd90>
@triple 装饰了 knock(),然后被 wrapper_triple() 内部函数替换,被装饰的函数的名字会变成装饰器函数,除了名称,还有文档字符串和其他元数据都将会被替换。但有时,我们并不总是想将被修饰的函数的所有信息都被修改了。此时 @functools.wraps 正好解决了这个问题,如下所示:
import functools def triple(func): @functools.wraps(func) def wrapper_triple(*args, **kwargs): print(f"Tripled {func.__name__!r}") value = func(*args, **kwargs) return value * 3 return wrapper_triple
使用 @triple 的这个新定义保留元数据:
@triple def knock(): return "Penny! " knock
<function knock at 0x7fa3bfe5df28>
注意knock() 即使在被装饰之后,也同样保留了它的原有函数名称。当定义装饰器时,使用 @functools.wraps 是一种不错的选择,可以为大多数装饰器使用的如下模板:
import functools def decorator(func): @functools.wraps(func) def wrapper_decorator(*args, **kwargs): # Do something before value = func(*args, **kwargs) # Do something after return value return wrapper_decorator
在本节中,云朵君将和大家一起学习如何扩展 Python 计时器,并以装饰器的形式使用它。接下来我们从头开始创建 Python 计时器装饰器。
根据上面的模板,我们只需要决定在调用装饰函数之前和之后要做什么。这与进入和退出上下文管理器时的注意事项类似。在调用修饰函数之前启动 Python 计时器,并在调用完成后停止 Python 计时器。可以按如下方式定义 @timer 装饰器:
import functools import time def timer(func): @functools.wraps(func) def wrapper_timer(*args, **kwargs): tic = time.perf_counter() value = func(*args, **kwargs) toc = time.perf_counter() elapsed_time = toc - tic print(f"Elapsed time: {elapsed_time:0.4f} seconds") return value return wrapper_timer
可以按如下方式应用 @timer:
@timer def download_data(): 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) download_data() # Python Timer Functions: Three Ways to Monitor Your Code
[ ... ] Elapsed time: 0.5414 seconds
回想一下,还可以将装饰器应用于先前定义的下载数据的函数:
requests.get = requests.get(source_url, headers=headers)
使用装饰器的一个优点是只需要应用一次,并且每次都会对函数计时:
data = requests.get(0)
Elapsed time: 0.5512 seconds
虽然@timer 顺利完成了对目标函数的定时。但从某种意义上说,你又回到了原点,因为该装饰器 @timer 失去了前面定义的类 Timer 的灵活性或便利性。换句话说,我们需要将 Timer 类表现得像一个装饰器。
现在我们似乎已经将装饰器用作应用于其他函数的函数,但其实不然,因为装饰器必须是可调用的。Python中有许多可调用的类型,可以通过在其类中定义特殊的.__call__()方法来使自己的对象可调用。以下函数和类的行为类似:
def square(num): return num ** 2 square(4)
16
class Squarer: def __call__(self, num): return num ** 2 square = Squarer() square(4)
16
这里,square 是一个可调用的实例,可以对数字求平方,就像square()第一个示例中的函数一样。
我们现在向现有Timer类添加装饰器功能,首先需要 import functools。
# timer.py import functools # ... @dataclass class Timer: # The rest of the code is unchanged def __call__(self, func): """Support using Timer as a decorator""" @functools.wraps(func) def wrapper_timer(*args, **kwargs): with self: return func(*args, **kwargs) return wrapper_timer
在之前定义的上下文管理器 Timer ,给我们带来了不少便利。而这里使用的装饰器,似乎更加方便。
@Timer(text="Downloaded the tutorial in {:.2f} seconds") def download_data(): 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) download_data() # Python Timer Functions: Three Ways to Monitor Your Code
[ ... ] Downloaded the tutorial in 0.72 seconds
有一种更直接的方法可以将 Python 计时器变成装饰器。其实上下文管理器和装饰器之间的一些相似之处:它们通常都用于在执行某些给定代码之前和之后执行某些操作。
基于这些相似之处,在 python 标准库中定义了一个名为 ContextDecorator 的 mixin 类,它可以简单地通过继承 ContextDecorator 来为上下文管理器类添加装饰器函数。
from contextlib import ContextDecorator # ... @dataclass class Timer(ContextDecorator): # Implementation of Timer is unchanged
当以这种方式使用 ContextDecorator 时,无需自己实现 .__call__(),因此我们可以大胆地将其从 Timer 类中删除。
接下来,再最后一次重改 download_data.py 示例,使用 Python 计时器作为装饰器:
# download_data.py import requests from timer import Timer @Timer() 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()
我们与之前的写法进行比较,唯一的区别是第 3 行的 Timer 的导入和第 4 行的 @Timer() 的应用。使用装饰器的一个显着优势是它们通常很容易调用。
但是,装饰器仍然适用于整个函数。这意味着代码除了记录了下载数据所需的时间外,还考虑了保存数据所需的时间。运行脚本:
$ python download_data.py # Python Timer Functions: Three Ways to Monitor Your Code
[ ... ] Elapsed time: 0.69 seconds
从上面打印出来的结果可以看到,代码记录了下载数据和保持数据一共所需的时间。
当使用 Timer 作为装饰器时,会看到与使用上下文管理器类似的优势:
然而,装饰器不如上下文管理器灵活,只能将它们应用于完整函数。
这里展开下面的代码块以查看 Python 计时器timer.py的完整源代码。
上下滑动查看更多源码
# timer.py import time from contextlib import ContextDecorator from dataclasses import dataclass, field from typing import Any, Callable, ClassVar, Dict, Optional class TimerError(Exception): """A custom exception used to report errors in use of Timer class""" @dataclass class Timer(ContextDecorator): """Time your code using a class, context manager, or decorator""" 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: """Initialization: add timer to dict of timers""" if self.name: 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 def __enter__(self) -> "Timer": """Start a new timer as a context manager""" self.start() return self def __exit__(self, *exc_info: Any) -> None: """Stop the context manager timer""" self.stop()
可以自己使用代码,方法是将其保存到一个名为的文件中timer.py并将其导入:
from timer import Timer
PyPI 上也提供了 Timer,因此更简单的选择是使用 pip 安装它:
pip install codetiming
注意,PyPI 上的包名称是codetiming,安装包和导入时都需要使用此名称Timer:
from codetiming import Timer
除了名称和一些附加功能之外,codetiming.Timer 与 timer.Timer 完全一样。总而言之,可以通过三种不同的方式使用 Timer:
1. 作为一个类:
t = Timer(name="class") t.start() # Do something t.stop()
2. 作为上下文管理器:
with Timer(name="context manager"): # Do something
3. 作为装饰器:
@Timer(name="decorator") def stuff(): # Do something
这种 Python 计时器主要用于监控代码在单个关键代码块或函数上所花费的时间。
Python定时器装饰器已经学习完毕了,接下来是总结了一些其他的 Python 定时器函数,如果你对其不太感兴趣,可以直接跳到最后。
使用 Python 对代码进行计时有很多选择。这里我们学习了如何创建一个灵活方便的类,可以通过多种不同的方式使用该类。对 PyPI 的快速搜索发现,已经有许多项目提供 Python 计时器解决方案。
在本节中,我们首先了解有关标准库中用于测量时间的不同函数的更多信息,包括为什么 perf_counter() 更好,然后探索优化代码的替代方案。
在本文之前,包括前面介绍python定时器的文章中,我们一直在使用 perf_counter() 来进行实际的时间测量,但是 Python 的时间库附带了几个其他也可以测量时间的函数。这里有一些:
拥有多个函数的一个原因是 Python 将时间表示为浮点数。浮点数本质上是不准确的。之前可能已经看到过这样的结果:
>>> 0.1 + 0.1 + 0.1 0.30000000000000004 >>> 0.1 + 0.1 + 0.1 == 0.3 False
Python 的 Float 遵循 IEEE 754 浮点算术标准[5],该标准以 64 位表示所有浮点数。因为浮点数有无限多位数,即不能用有限的位数来表达它们。
考虑time()这个函数的主要目的,是它表示的是现在的实际时间。它以自给定时间点(称为纪元)以来的秒数来表示函数。time()返回的数字很大,这意味着可用的数字较少,因而分辨率会受到影响。简而言之, time()无法测量纳秒级差异:
>>> import time >>> t = time.time() >>> t 1564342757.0654016 >>> t + 1e-9 1564342757.0654016 >>> t == t + 1e-9 True
一纳秒是十亿分之一秒。上面代码中,将纳秒添加到参数 t ,他并不会影响结果。与 time() 不同的是,perf_counter() 使用一些未定义的时间点作为它的纪元,它可以使用更小的数字,从而获得更好的分辨率:
>>> import time >>> p = time.perf_counter() >>> p 11370.015653846 >>> p + 1e-9 11370.015653847 >>> p == p + 1e-9 False
众所周知,将时间表示为浮点数是非常具有挑战的一件事,因此 Python 3.7 引入了一个新选项:每个时间测量函数现在都有一个相应的 _ns 函数,它以 int 形式返回纳秒数,而不是以浮点数形式返回秒数。例如,time() 现在有一个名为 time_ns() 的纳秒对应项:
import time time.time_ns()
1564342792866601283
整数在 Python 中是无界的,因此 time_ns() 可以为所有永恒提供纳秒级分辨率。同样,perf_counter_ns() 是 perf_counter() 的纳秒版本:
>>> import time >>> time.perf_counter() 13580.153084446 >>> time.perf_counter_ns() 13580765666638
我们注意到,因为 perf_counter() 已经提供纳秒级分辨率,所以使用 perf_counter_ns() 的优势较少。
注意: perf_counter_ns() 仅在 Python 3.7 及更高版本中可用。在 Timer 类中使用了 perf_counter()。这样,也可以在较旧的 Python 版本上使用 Timer。
有两个函数time不测量time.sleep时间:process_time()和thread_time()。通常希望Timer能够测量代码所花费的全部时间,因此这两个函数并不常用。而函数 monotonic(),顾名思义,它是一个单调计时器,一个永远不会向后移动的 Python 计时器。
除了 time() 之外,所有这些函数都是单调的,如果调整了系统时间,它也随之倒退。在某些系统上,monotonic() 与 perf_counter() 的功能相同,可以互换使用。我们可以使用 time.get_clock_info() 获取有关 Python 计时器函数的更多信息:
>>> import time >>> time.get_clock_info("monotonic") namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09) >>> time.get_clock_info("perf_counter") namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09)
注意,不同系统上的结果可能会有所不同。
PEP 418 描述了引入这些功能的一些基本原理。它包括以下简短描述:
在实际工作中,通常会想优化代码进一步提升代码性能,例如想知道将列表转换为集合的最有效方法。下面我们使用函数 set() 和直接花括号定义集合 {...} 进行比较,看看这两种方法哪个性能更优,此时需要使用 Python 计时器来比较两者的运行速度。
>>> from timer import Timer >>> numbers = [7, 6, 1, 4, 1, 8, 0, 6] >>> with Timer(text="{:.8f}"): ... set(numbers) ... {0, 1, 4, 6, 7, 8} 0.00007373 >>> with Timer(text="{:.8f}"): ... {*numbers} ... {0, 1, 4, 6, 7, 8} 0.00006204
该测试结果表明直接花括号定义集合可能会稍微快一些,但其实这些结果非常不确定。如果重新运行代码,可能会得到截然不同的结果。因为这会受计算机的性能和计算机运行状态所影响:例如当计算机忙于其他任务时,就会影响我们程序的结果。
更好的方法是多次重复运行相同过程,并获取平均耗时,就能够更加精确地测量目标程序的性能大小。因此可以使用 timeit 标准库,它旨在精确测量小代码片段的执行时间。虽然可以从 Python 导入和调用 timeit.timeit() 作为常规函数,但使用命令行界面通常更方便。可以按如下方式对这两种变体进行计时:
$ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "set(nums)" 2000000 loops, best of 5: 163 nsec per loop $ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "{*nums}" 2000000 loops, best of 5: 121 nsec per loop
timeit 自动多次调用代码以平均噪声测量。timeit 的结果证实 {*nums} 量比 set(nums) 快。
注意:在下载文件或访问数据库的代码上使用 timeit 时要小心。由于 timeit 会自动多次调用程序,因此可能会无意中向服务器发送请求!
最后,IPython 交互式 shell 和 Jupyter Notebook 使用 %timeit 魔术命令对此功能提供了额外支持:
In [1]: numbers = [7, 6, 1, 4, 1, 8, 0, 6] In [2]: %timeit set(numbers) 171 ns ± 0.748 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) In [3]: %timeit {*numbers} 147 ns ± 2.62 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
同样,测量结果表明直接花括号定义集合更快。在 Jupyter Notebooks 中,还可以使用 %%timeit cell-magic 来测量运行整个单元格的时间。
timeit 非常适合对特定代码片段进行基准测试。但使用它来检查程序的所有部分并找出哪些部分花费的时间最多会非常麻烦。此时我们想到可以使用分析器。
cProfile 是一个分析器,可以随时从标准库中访问它。可以通过多种方式使用它,尽管将其用作命令行工具通常是最直接的:
$ python -m cProfile -o download_data.prof download_data.py
此命令在打开分析器的情况下运行 download_data.py。将 cProfile 的输出保存在 download_data.prof 中,由 -o 选项指定。输出数据是二进制格式,需要专门的程序才能理解。同样,Python 在标准库中有一个选项 pstats!它可以在 .prof 文件上运行 pstats 模块会打开一个交互式配置文件统计浏览器。
$ python -m pstats download_data.prof Welcome to the profile statistics browser. download_data.prof% help ...
要使用 pstats,请在提示符下键入命令。通常你会使用 sort 和 stats 命令,strip 可以获得更清晰的输出:
download_data.prof% strip download_data.prof% sort cumtime download_data.prof% stats 10 ...
此输出显示总运行时间为 0.586 秒。它还列出了代码花费最多时间的十个函数。这里按累积时间 ( cumtime) 排序,这意味着当给定函数调用另一个函数时,代码会计算时间。
总时间 ( tottime) 列表示代码在函数中花费了多少时间,不包括在子函数中的时间。要查找代码花费最多时间的位置,需要发出另一个sort命令:
download_data.prof% sort tottime download_data.prof% stats 10 ...
可以使用 pstats了解代码大部分时间花在哪里,然后尝试优化我们发现的任何瓶颈。还可以使用该工具更好地理解代码的结构。例如,被调用者和调用者命令将显示给定函数调用和调用的函数。
还可以研究某些函数。通过使用短语 timer 过滤结果来检查 Timer 导致的开销:
download_data.prof% stats timer ...
完成调查后,使用 quit 离开 pstats 浏览器。
如需更加深入了解更强大的配置文件数据接口,可以查看 KCacheGrind[8]。它使用自己的数据格式,也可以使用 pyprof2calltree[9] 从 cProfile 转换数据:
$ pyprof2calltree -k -i download_data.prof
该命令将转换 download_data.prof 并打开 KCacheGrind 来分析数据。
这里为代码计时的最后一个选项是 line_profiler[10]。cProfile 可以告诉我们代码在哪些函数中花费的时间最多,但它不会深入显示该函数中的哪些行最慢,此时就需要 line_profiler 。
注意:还可以分析代码的内存消耗。这超出了本教程的范围,如果你需要监控程序的内存消耗,可以查看 memory-profiler[11] 。
行分析需要时间,并且会为我们的运行时增加相当多的开销。正常的工作流程是首先使用 cProfile 来确定要调查的函数,然后在这些函数上运行 line_profiler。line_profiler 不是标准库的一部分,因此应该首先按照安装说明[12]进行设置。
在运行分析器之前,需要告诉它要分析哪些函数。可以通过在源代码中添加 @profile 装饰器来实现。例如,要分析 Timer.stop(),在 timer.py 中添加以下内容:
@profile def stop(self) -> float: # 其余部分不变
注意,不需要导入profile配置文件,它会在运行分析器时自动添加到全局命名空间中。不过,我们需要在完成分析后删除该行。否则,会抛出一个 NameError 异常。
接下来,使用 kernprof 运行分析器,它是 line_profiler 包的一部分:
$ kernprof -l download_data.py
此命令自动将探查器数据保存在名为 download_data.py.lprof 的文件中。可以使用 line_profiler 查看这些结果:
$ python -m line_profiler download_data.py.lprof Timer unit: 1e-06 s Total time: 1.6e-05 s File: /home/realpython/timer.py Function: stop at line 35 # Hits Time PrHit %Time Line Contents ===================================== ...
首先,注意本报告中的时间单位是微秒(1e-06 s)。通常,最容易查看的数字是 %Time,它告诉我们代码在每一行的函数中花费的总时间的百分比。
在本文中,我们尝试了几种不同的方法来将 Python 计时器添加到代码中:
我们还了解了为什么在对代码进行基准测试时应该更喜欢time.perf_counter()而不是 time.time(),以及在优化代码时还有哪些其他有用的替代方法。
现在我们可以在自己的代码中添加Python计时器函数了!在日志中跟踪程序的运行速度将有助于监视脚本。
위 내용은 데코레이터를 사용하여 Python 타이머를 확장하는 방법을 단계별로 가르쳐주세요.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!