有什么简单的方法来测试 Python 脚本吗?

通常我使用 shell 命令 time。我的目的是测试数据是小的、中等的、大的还是非常大的集合,以及将会占用多少时间和内存。

Linux 或者 Python 有什么工具可以做到这一点吗?

111548 次浏览

看看 timeit巨蟒分析器印刷字体。还要确保看到提到“ SnakeViz”的 以下是 nikicc的评论。它为您提供了另一个可视化的分析数据,这可能是有帮助的。

计时

def test():
"""Stupid test function"""
lst = []
for i in range(100):
lst.append(i)


if __name__ == '__main__':
import timeit
print(timeit.timeit("test()", setup="from __main__ import test"))


# For Python>=3.5 one can also write:
print(timeit.timeit("test()", globals=locals()))

实际上,您可以将 python 代码作为字符串参数传递给它,它将以指定的次数运行并输出执行时间。来自 docs的重要信息:

timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None) 使用给定的语句 设计创建一个 Timer实例 代码和 计时器函数,并使用以下命令运行其 timeit方法 可选的 全球化参数指定执行代码的名称空间。

... and:

Timer.timeit(number=1000000) 执行主语句的 number时间。这将执行设置 语句,然后返回执行 main 所需的时间 语句,以秒为单位作为浮点数进行测量。 参数是通过循环的次数,默认为1 主语句、设置语句和计时器函数 传递给构造函数。

注: 默认情况下,timeit在定时期间暂时关闭 garbage collection 它使独立的时间更具可比性。这个缺点是 GC 可能是性能的一个重要组成部分 如果是这样,GC 可以作为第一个 例如:

timeit.Timer('for i in xrange(10): oct(i)', 'gc.enable()').timeit()

侧写

分析会让你对 很多有更详细的了解。下面是来自 官方文件的“即时例子”:

import cProfile
import re
cProfile.run('re.compile("foo|bar")')

这会给你:

      197 function calls (192 primitive calls) in 0.002 seconds


Ordered by: standard name


ncalls  tottime  percall  cumtime  percall filename:lineno(function)
1    0.000    0.000    0.001    0.001 <string>:1(<module>)
1    0.000    0.000    0.001    0.001 re.py:212(compile)
1    0.000    0.000    0.001    0.001 re.py:268(_compile)
1    0.000    0.000    0.000    0.000 sre_compile.py:172(_compile_charset)
1    0.000    0.000    0.000    0.000 sre_compile.py:201(_optimize_charset)
4    0.000    0.000    0.000    0.000 sre_compile.py:25(_identityfunction)
3/1    0.000    0.000    0.000    0.000 sre_compile.py:33(_compile)

这两个模块都可以让您了解在哪里查找瓶颈。

Also, to get to grips with the output of profile, have a look at 这篇文章

pycallgraph

注意: 字符串已被正式废弃。不过,到2020年12月为止,它仍然在编写 Python 3.6。不过,只要 python 公开分析 API 的方式没有发生核心变化,它仍然应该是一个有用的工具。

这个模块 使用 Graphviz 创建如下的调用:

callgraph example

你可以很容易地看到哪些路径使用了最多的时间,由颜色。您可以使用 pycallgraph API 创建它们,也可以使用打包的脚本:

pycallgraph graphviz -- ./mypythonscript.py

不过,开销是相当可观的。因此,对于已经长时间运行的流程,创建图表可能需要一些时间。

Have a look at 鼻子 and at one of its plugins, this one in particular.

安装之后,鼻子就是您路径中的一个脚本,您可以在包含一些 python 脚本的目录中调用该脚本:

$: nosetests

这将查看工作目录中的所有 python 文件,并执行它识别为测试的任何函数,例如,它识别任何名称中带有单词 test _ 的函数作为测试。

因此,您可以创建一个名为 test _ yourfunction. py 的 Python 脚本,并在其中编写如下代码:

$: cat > test_yourfunction.py


def test_smallinput():
yourfunction(smallinput)


def test_mediuminput():
yourfunction(mediuminput)


def test_largeinput():
yourfunction(largeinput)

那你就得逃跑

$: nosetest --with-profile --profile-stats-file yourstatsprofile.prof testyourfunction.py

要读取配置文件,请使用以下 Python 代码行:

python -c "import hotshot.stats ; stats = hotshot.stats.load('yourstatsprofile.prof') ; stats.sort_stats('time', 'calls') ; stats.print_stats(200)"

我通常做一个快速 time ./script.py看看需要多长时间。但是这不会显示内存,至少不是默认的。可以使用 /usr/bin/time -v ./script.py获取大量信息,包括内存使用情况。

我用一个简单的装饰器来计时

import time


def st_time(func):
"""
st decorator to calculate the total time of a func
"""


def st_func(*args, **keyArgs):
t1 = time.time()
r = func(*args, **keyArgs)
t2 = time.time()
print("Function=%s, Time=%s" % (func.__name__, t2 - t1))
return r


return st_func

timeit模块又慢又奇怪,所以我写了这个:

def timereps(reps, func):
from time import time
start = time()
for i in range(0, reps):
func()
end = time()
return (end - start) / reps

例如:

import os
listdir_time = timereps(10000, lambda: os.listdir('/'))
print "python can do %d os.listdir('/') per second" % (1 / listdir_time)

对我来说,它说:

python can do 40925 os.listdir('/') per second

这是一种原始的基准测试,但已经足够好了。

所有内存需求的内存分析器。

Https://pypi.python.org/pypi/memory_profiler

运行 pip 安装:

pip install memory_profiler

导入图书馆:

import memory_profiler

向您希望配置的项目添加一个装饰器:

@profile
def my_func():
a = [1] * (10 ** 6)
b = [2] * (2 * 10 ** 7)
del b
return a


if __name__ == '__main__':
my_func()

执行代码:

python -m memory_profiler example.py

接收输出:

 Line #    Mem usage  Increment   Line Contents
==============================================
3                           @profile
4      5.97 MB    0.00 MB   def my_func():
5     13.61 MB    7.64 MB       a = [1] * (10 ** 6)
6    166.20 MB  152.59 MB       b = [2] * (2 * 10 ** 7)
7     13.61 MB -152.59 MB       del b
8     13.61 MB    0.00 MB       return a

例子来自上面链接的文档。

小心 timeit非常慢,在我的中型处理器上初始化(或者运行函数)需要12秒。你可以测试这个被接受的答案

def test():
lst = []
for i in range(100):
lst.append(i)


if __name__ == '__main__':
import timeit
print(timeit.timeit("test()", setup="from __main__ import test")) # 12 second

对于简单的事情,我将使用 time代替,在我的电脑上它返回的结果 0.0

import time


def test():
lst = []
for i in range(100):
lst.append(i)


t1 = time.time()


test()


result = time.time() - t1
print(result) # 0.000000xxxx

快速测试任何函数的简单方法是使用以下语法: %timeit my_code

For instance :

%timeit a = 1


13.4 ns ± 0.781 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)

如果您不想为时间编写样板代码,而且不想轻松地分析结果,那么可以看一下 基准。此外,它还保存了以前运行的历史,因此很容易在开发过程中比较相同的功能。

# pip install benchmarkit


from benchmarkit import benchmark, benchmark_run


N = 10000
seq_list = list(range(N))
seq_set = set(range(N))


SAVE_PATH = '/tmp/benchmark_time.jsonl'


@benchmark(num_iters=100, save_params=True)
def search_in_list(num_items=N):
return num_items - 1 in seq_list


@benchmark(num_iters=100, save_params=True)
def search_in_set(num_items=N):
return num_items - 1 in seq_set


benchmark_results = benchmark_run(
[search_in_list, search_in_set],
SAVE_PATH,
comment='initial benchmark search',
)

打印到终端并返回包含上次运行数据的字典列表。命令行入口点也可用。

enter image description here

如果更改 N=1000000并重新运行

enter image description here

用于 cProfile 的 snakeviz交互式查看器

Https://github.com/jiffyclub/snakeviz/

https://stackoverflow.com/a/1593034/895245在评论中提到了 snakeviz中提到了 cProfile,但是我想进一步强调它。

It is very hard to debug program performance just by looking at cprofile / pstats output, because they can only total times per function out of the box.

However, what we really need in general is to see a nested view containing the stack traces of each call to actually find the main bottlenecks easily.

而这正是 snakeviz 通过其默认的“冰柱”视图提供的。

首先,您必须将 cProfile 数据转储到一个二进制文件,然后您可以在该文件上使用 snakeviz

pip install -u snakeviz
python -m cProfile -o results.prof myscript.py
snakeviz results.prof

这将打印一个到标准输出的 URL,您可以在浏览器上打开该 URL,其中包含如下所示的所需输出:

enter image description here

and you can then:

  • hover each box to see the full path to the file that contains the function
  • 单击一个框,使该框显示在顶部作为一种方式放大

More profile oriented question: 如何分析 Python 脚本?

根据刘的回答和一些方便的特点,也许对某些人是有用的。

def stopwatch(repeat=1, autorun=True):
"""
stopwatch decorator to calculate the total time of a function
"""
import timeit
import functools
    

def outer_func(func):
@functools.wraps(func)
def time_func(*args, **kwargs):
t1 = timeit.default_timer()
for _ in range(repeat):
r = func(*args, **kwargs)
t2 = timeit.default_timer()
print(f"Function={func.__name__}, Time={t2 - t1}")
return r
        

if autorun:
try:
time_func()
except TypeError:
raise Exception(f"{time_func.__name__}: autorun only works with no parameters, you may want to use @stopwatch(autorun=False)") from None
        

return time_func
    

if callable(repeat):
func = repeat
repeat = 1
return outer_func(func)
    

return outer_func

一些测试:

def is_in_set(x):
return x in {"linux", "darwin"}


def is_in_list(x):
return x in ["linux", "darwin"]


@stopwatch
def run_once():
import time
time.sleep(0.5)


@stopwatch(autorun=False)
def run_manually():
import time
time.sleep(0.5)


run_manually()


@stopwatch(repeat=10000000)
def repeat_set():
is_in_set("windows")
is_in_set("darwin")


@stopwatch(repeat=10000000)
def repeat_list():
is_in_list("windows")
is_in_list("darwin")


@stopwatch
def should_fail(x):
pass

结果:

Function=run_once, Time=0.5005391679987952
Function=run_manually, Time=0.500624185999186
Function=repeat_set, Time=1.7064883739985817
Function=repeat_list, Time=1.8905151920007484
Traceback (most recent call last):
(some more traceback here...)
Exception: should_fail: autorun only works with no parameters, you may want to use @stopwatch(autorun=False)

Line _ profiler (逐行执行时间)

装置

pip install line_profiler

用法

  • 在函数之前添加一个 @profile修饰符。例如:
@profile
def function(base, index, shift):
addend = index << shift
result = base + addend
return result
  • 使用命令 kernprof -l <file_name>创建 line _ profiler 的实例,例如:
kernprof -l test.py

Kernprof 将在成功时打印 Wrote profile results to <file_name>.lprof,例如:

Wrote profile results to test.py.lprof
  • 使用命令 python -m line_profiler <file_name>.lprof打印基准测试结果,例如:
python -m line_profiler test.py.lprof

您将看到关于每行代码的详细信息:

Timer unit: 1e-06 s


Total time: 0.0021632 s
File: test.py
Function: function at line 1


Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
1                                           @profile
2                                           def function(base, index, shift):
3      1000        796.4      0.8     36.8      addend = index << shift
4      1000        745.9      0.7     34.5      result = base + addend
5      1000        620.9      0.6     28.7      return result

Memory _ profiler (逐行使用内存)

装置

pip install memory_profiler

用法

  • Add a @profile decorator before function. For example:
@profile
def function():
result = []
for i in range(10000):
result.append(i)
return result
  • 使用命令 python -m memory_profiler <file_name>打印基准测试结果,例如:
python -m memory_profiler test.py

您将看到关于每行代码的详细信息:

Filename: test.py


Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
1   40.246 MiB   40.246 MiB           1   @profile
2                                         def function():
3   40.246 MiB    0.000 MiB           1       result = []
4   40.758 MiB    0.008 MiB       10001       for i in range(10000):
5   40.758 MiB    0.504 MiB       10000           result.append(i)
6   40.758 MiB    0.000 MiB           1       return result

好的练习

多次调用一个函数以尽量减少对环境的影响。