Python:统计给定行执行的次数
问题
为了教学目的,我想要计算在一个特定的函数中,某一行被执行的次数,但不想修改或装饰这个函数。比如,对于下面这个函数:
def binary_search(seq, x):
(a, b) = (0, len(seq) - 1)
while a <= b:
m = (a + b) / 2
if x < seq[m]:
b = m - 1
elif x > seq[m]:
a = m + 1
else:
return m
我只需要写类似这样的代码:
print count_exec(binary_search, range(100), 44, line_number = 4)
...或者这样:
print count_exec(binary_search(range(100), 44), line = "m = (a + b) / 2")
...这两种方式都应该能打印出第4行被执行的次数(也就是7次)。我的最终目标是提供一种经验方法来分析任何函数的复杂度:
不可行的方案
我现在的方案是给函数添加一个属性:
def binary_search(seq, x):
binary_search.count = 0 # <------------------ added
(a, b) = (0, len(seq) - 1)
while a <= b:
binary_search.count += 1 # <------------- added
m = (a + b) / 2
if x < seq[m]:
b = m - 1
elif x > seq[m]:
a = m + 1
else:
return m
binary_search(range(100), 44)
print binary_search.count
我想我可以创建一个装饰过的函数 count_this_line
:
def binary_search(seq, x):
(a, b) = (0, len(seq) - 1)
while a <= b:
count_this_line() # <-------------------- added
m = (a + b) / 2
...
也许可以直接装饰 binary_search
这个函数,但对我来说,这算是修改它了。
想法
- 标准库中的 ast 可以获取任何脚本的抽象语法树,甚至可以执行它。
- 我对使用 Python 性能分析工具的经验不多。感觉这对我来说有点复杂。这样做是否可行呢?
2 个回答
2
根据Jason的建议,我写了一个纯Python的解决方案:
import line_profiler
import __builtin__
import cStringIO
import re
def profile(path, function_call, line_number):
prof = line_profiler.LineProfiler()
__builtin__.__dict__['profile'] = prof
script = open(path).read()
ns = locals()
function_name = function_call[:function_call.index("(")]
rex = re.compile("((?ms)^def %s.+)" % function_name)
script = rex.sub(r"@profile\n\1\n%s" % function_call, script)
exec(script, ns, ns)
stream = cStringIO.StringIO()
prof.print_stats(stream)
s = stream.getvalue()
stream.close()
return int(re.search(r"(?m)^\s*%s\s*(\S*)" % (line_number+1), s).group(1))
if __name__ == "__main__":
print profile("binary_search.py", "binary_search(range(100), 44)", 3)
这个程序会读取包含要分析的函数的脚本源代码,然后给这个函数加上一些额外的功能,最后在代码的末尾添加一个调用这个函数的指令,执行它,结果会把统计信息放到一个字符串里,从中提取出指定行号的调用次数,并把这个数字作为一个整数返回。这个方法能达到要求,但性能上有点损失。
也许更好的解决方案是放弃这个分析工具,但保留动态装饰和执行源代码的想法。如果我实现了这个方案,我会更新我的回答。
总之,感谢Jason给我提供了一个解决的思路!
2
你可以使用 line_profiler
模块来实现这个功能(查看文档)。需要注意的是,我从一个分支库获取了一个兼容3.x版本的,不确定它是否已经合并到主版本中。
举个例子,我把你的二分查找函数放在一个文件里,然后加上了这个:
prof = profile(binary_search)
prof(range(100), 44)
这和文档中提到的 @profile
装饰器是一样的,但你不需要修改原始代码。我运行了
kernprof.py -l binsearch.py
python -m line_profiler binsearch.py.lprof
然后得到了这个结果:
Function: binary_search at line 1
Total time: 4.1e-05 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
1 def binary_search(seq, x):
2 1 6 6.0 14.6 (a, b) = (0, len(seq) - 1)
3 7 8 1.1 19.5 while a <= b:
4 7 7 1.0 17.1 m = (a + b) // 2
5 7 8 1.1 19.5 if x < seq[m]:
6 2 2 1.0 4.9 b = m - 1
7 5 5 1.0 12.2 elif x > seq[m]:
8 4 4 1.0 9.8 a = m + 1
9 else:
10 1 1 1.0 2.4 return m
这里的“Hits”就是你要找的数字。作为额外的好处,你还可以得到一些时间信息,不过如果执行多次,时间信息会更准确。