欢迎关注 “小白玩转Python”,发现更多 “有趣”
引言
测试代码的速度和效率是软件开发的一个重要方面。当代码占用太长时间或者消耗太多资源(如内存或 CPU)时,可能会很快遇到各种问题,例如:代码运行的机器可能变得不稳定,在某些情况下甚至会丢失数据。确保在出现明显的性能问题时进行检查是有帮助的,但是建立性能基准和概要也同样重要。
在开发过程中,应该对代码从开始到结束的功能进行测试,但是对性能进行测试也很重要。在编写代码时养成测试代码的好习惯,比如速度和资源利用率,这会让你在编写代码的过程中省去很多麻烦。
在本文中,我们将探索可以对 Python 代码进行基准测试和基线化的方法。我们将要介绍的库是免费提供的,并且提供了灵活的方法来处理性能计时、资源消耗度量等等。让我们开始吧。
1.计时
首先是一个 Python 实用程序,它被广泛应用于性能测试。让我们设置一个简单的测试脚本并使用 timeit,来进行一个简单的时间测试:
#!/usr/bin/env python3
# test.py
import timeit
import time
def long_function():
print('function start')
time.sleep(5)
print('function end')
print(timeit.Timer(long_function).timeit(number=2))
在我们的 long_function 内部,我们使用 time.sleep 引入了一些延迟,以便模拟一些长时间运行的任务。接下来,为了实际测试我们的功能,我们将其传递给timeit.Timer。Timer 类测量函数的整体执行速度。number 参数指定应重复测试多少次。如果你的函数执行时间可能略有不同,这将很有用。重复进行测试可以更好地了解速度,因为你将拥有更多可处理的数据。
运行上面的代码后,我们可以获得以下输出。这表明 timeit 运行了两次函数,然后输出了所花费的总时间。由于我们只是在调用 sleep,所以差异不会太大:
function start
function end
function start
function end
10.00957689608913
Timeit 库非常适合对代码片段执行快速、独立的测试,但也可以作为独立单元从命令行运行,并包含更多有用的参数。
2.行剖析器
我们将探索的下一个库名为 line_profiler,它的用法比其他解决方案更加独特。line_profiler 库允许你获取文件中每一行的执行时间。看到每一行所花费的时间,就可以快速地查明问题,而不是一行一行地查找密集的代码。
一开始,line_profiler 的标准用法似乎有点混乱,但是一旦你使用了几次,它就变得更容易了。为了对代码进行分析,需要在每个函数中添加@profile装饰器。让我们重新使用之前的例子并对其进行调整,看看它是如何工作的:
#!/usr/bin/env python3
# test.py
import time
@profile
def long_function():
print('function start')
time.sleep(5)
print('function end')
long_function()
看起来很简单,对吧?这是因为使用 line_profiler,你将不必导入任何内容或大量更改代码,只需添加装饰器即可。因为我们已经设置了装饰器,所以剩下的唯一要做的事情就是测试代码。为了运行 line_profiler,必须做另外两件事:
kernprof -l test.py
python -m line_profiler test.py.lprof
上面的第一个命令将在文件上实际运行 line_profiler 并在同一目录中生成一个单独的 .lprof 文件。.lprof 文件包含结果,使用第二个命令中的模块本身可以生成报告。让我们看一下第二个命令的输出:
Timer unit: 1e-06 s
Total time: 5.00472 s
File: test.py
Function: long_function at line 6
Line # Hits Time Per Hit % Time Line Contents
==============================================================
6 @profile
7 def long_function():
8 1 15.0 15.0 0.0 print('function start')
9 1 5004679.0 5004679.0 100.0 time.sleep(5)
10 1 21.0 21.0 0.0 print('function end')
概要分析功能的每一行均列出了其详细统计信息。因为我们在 long_function 的 sleep 中花费了很多时间,所以它几乎占用了文件执行时间的100%。使用 line_profiler 生成所有执行时间去向的分解,让你快速确定可能需要专注于重构的位置。
3.资源
对于接下来的两个库,我们将重点关注运行 Python 代码所涉及的底层资源。在现代环境中,能够告诉代码使用了多少 CPU 和内存几乎是必需的。这个库允许你测量代码中的资源使用情况,甚至可以设置特定资源的使用限制。
让我们来看一个如何从脚本中检查 CPU 使用情况的例子:
#!/usr/bin/env python3
# test.py
import time
from resource import getrusage, RUSAGE_SELF
def long_function():
for i in range(10 ** 10):
2 + 2
long_function()
print(getrusage(RUSAGE_SELF))
在此示例中,为了在 long_function 期间给 CPU 带来更大的负担,我们在较大范围的数字上循环并强制 CPU 执行一些计算。这将产生比 sleep 更高的负载。
完成上述测试之后,我们应该能够看到下面的使用输出:
resource.struct_rusage(ru_utime=152.395004, ru_stime=0.035994, ru_maxrss=8536, ru_ixrss=0, ru_idrss=0, ru_isrss=0, ru_minflt=1092, ru_majflt=0, ru_nswap=0, ru_inblock=0, ru_oublock=0, ru_msgsnd=0, ru_msgrcv=0, ru_nsignals=0, ru_nvcsw=0, ru_nivcsw=1604)
在这个输出中可以看到,我们花费了大量的 CPU 周期。查看 ru_utime (用户时间) ,它显示代码总共花费了152秒的时间。当然我们不仅可以测量 CPU 时间,还可以通过其他一些指标了解块 IO 和堆栈内存使用情况。
4.内存分析器
Memory_profiler 库类似于 line_profiler,但侧重于生成与内存使用直接相关的统计数据。在代码上运行 memory_profiler 时,仍然会得到逐行分解,但是将重点放在整体内存使用和逐行增量内存使用上。
就像在 line_profiler 中一样,我们将使用相同的装饰器结构来测试我们的代码。这是修改后的示例代码:
#!/usr/bin/env python3
# test.py
@profile
def long_function():
data = []
for i in range(100000):
data.append(i)
return data
long_function()
在上面的示例中,我们创建了一个测试列表并将大量整数推入其中。这会使列表缓慢增加,因此我们可以看到内存使用量随时间增长。为了查看 memory_profiler 报告,我们可以简单地运行:
python -m memory_profiler test.py
这将产生以下报告,其中包含逐行内存统计信息:
Filename: tat.py
Line # Mem usage Increment Occurences Line Contents
============================================================
3 38.207 MiB 38.207 MiB 1 @profile
4 def long_function():
5 38.207 MiB 0.000 MiB 1 data = []
6 41.934 MiB 2.695 MiB 100001 for i in range(100000):
7 41.934 MiB 1.031 MiB 100000 data.append(i)
8 41.934 MiB 0.000 MiB 1 return data
如上所示,我们的函数从大约38 MB 的内存使用开始,在我们的列表填满之后增长到41.9 MB。虽然我们可以从 resource 库中获得内存使用情况信息,但是它不会像 memory_profiler 那样产生详细的逐行分解。如果你正在查找内存泄漏或处理一个特别臃肿的应用程序,这将是一个非常好的方法。
· END ·
HAPPY LIFE