debug

__debug__ 是 Python 中的一个内置常量(Built-in Constants)。如果执行 Python 脚本时没有带上 -O(optimize generated bytecode slightly)选项,那么该变量就为 true。

__debug__ 常量常与 assert 搭配使用,例如:

1
2
if __debug__:
if not expression: raise AssertionError

单元测试

详见单元测试

性能测试(Profiling)

在讨论性能测试之前,注意一些有利于提高程序性能的设计习惯:

  1. 在需要只读序列时,最好使用元组而非列表;
  2. 使用生成器,而不是创建大的元组和列表并在其上进行迭代操作;
  3. 尽量使用 Python 内置的数据结构(字典、列表、元组等)而避免自定义结构,因为内置的数据结构都是经过了高度优化的;
  4. 从小字符串中产生大字符串时,不要对小字符串进行连接,而是在列表中累积,最后将该字符串列表结合成为一个单独的字符串;
  5. 如果某个对象(包括函数或方法)需要多次使用属性进行访问(比如访问模块中的某个函数),或从某个数据结构中进行访问,那么较好的做法是创建并使用一个局部变量来访问该对象,以便提供更快的访问速度。

Python 标准库中提供了两个特别有用的用于性能测试的模块:

  1. timeit 模块 —— 该模块可用于对一小段 Python 代码进行计时,并可用于诸如对两个或多个特定函数或方法的性能进行比较等场合。
  2. cProfile 模块 —— 该模块对调用计数与次数进行了详细分解,以便发现性能瓶颈所在。

timeit

示例1

1
2
3
4
5
6
7
>>> import timeit
>>> timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
0.8187260627746582
>>> timeit.timeit('"-".join([str(n) for n in range(100)])', number=10000)
0.7288308143615723
>>> timeit.timeit('"-".join(map(str, range(100)))', number=10000)
0.5858950614929199

其中,number参数是该测试语句重复执行的次数。

示例2

下列代码示例了利用 timeit 同时对3个函数进行计时:

1
2
3
4
5
6
7
8
9
import timeit
if __name__ == "__main__"
repeats = 1000
for function in ("function_a", "function_b", "function_c"):
t = timeit.Timeer("{}(X, Y)".format(function),
"from __main__ import {0}, X, Y".format(function))
sec = t.timeit(repeats) / repeats
print("{function}() {sec:.6f} sec.format(**locals()))"
)

示例3:直接在命令行对代码进行即时

1
2
3
4
5
6
$ python -m timeit '"-".join(str(n) for n in range(100))'
10000 loops, best of 3: 40.3 usec per loop
$ python -m timeit '"-".join([str(n) for n in range(100)])'
10000 loops, best of 3: 33.4 usec per loop
$ python -m timeit '"-".join(map(str, range(100)))'
10000 loops, best of 3: 25.2 usec per loop

更多示例可以参见官方文档上的例子

cProfile

cProfile(或者 profile 模块,这里统称为 cProfile 模块[1])也可以用于比较函数与方法的性能。与只是提供原始计时的 timeit 模块不同的是, cProfile 模块精确地展示了有什么被调用以及每个调用耗费了多少时间。

示例1

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

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
  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)

每列的含义如下:

  1. ncalls列:调用次数;
  2. tottime列:列出了每个函数中耗费的总时间,排除了调用子函数花费的时间(excluding time made in calls to sub-functions);
  3. 第一个percall列:列出了对函数的每次执行的平均时间(tottime // ncalls)。
  4. cumtime:列出了在函数中耗费的时间,并且包含了调用子函数的花费的时间。
  5. 第二个percall列:列出了对函数的每次执行的平均时间,包含了调用子函数的花费的时间。
  6. filename:lineno(function)列:提供每个函数的相关信息。

示例2

可以将性能测试结果改为输出到文件中。只需要指定一个文件名即可。参见下面的示例:

1
2
3
import cProfile
import re
cProfile.run('re.compile("foo|bar")', 'restats')

pstats.Stats类可以用于读取性能测试结果并以多种方式格式化它。

示例3

下面是用于比较与前面一样的3个函数的代码:

1
2
3
4
5
import cProfile
if __name__ = "__main__"
for function in ("function_a", "function_b", "function_c"):
cProfile.run("for i in range(1000):{0}(X, Y)"
.format(function))

示例4:直接在命令行使用 cProfile

1
python -m cProfile [-o output_file] [-s sort_order] myscript.py
  • -o 选项:将测试结果写入到文件,而不是通过标准输出;
  • -s 选项:通过指定一个 sort_stats() 排序值来对输出进行排序,仅当没有使用 -o 选项时才有效。

关于cProfile的详细介绍可以参见官方文档

补充材料

  1. Built-in Constants
  2. timeit
  3. cProfile

  1. 两者功能上没什么区别,如果你的系统无法调用cProfile,就调用 profile。 ↩︎

Comments