Python:计算给定行执行的次数

时间:2014-08-13 13:16:39

标签: python profiling metaprogramming decorator abstract-syntax-tree

问题

出于教学目的,我想计算一个给定行在给定函数中执行多少次而不修改或装饰它。例如,对于函数:

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)。最终目标是为任何功能的复杂性提供经验方法:

Complexity of binary search

非解决方案

我目前的解决方案是添加一个函数属性:

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 个答案:

答案 0 :(得分:2)

您可以使用line_profiler模块执行此操作(see docs)。请注意,我必须从forked repo获得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

&#34;命中&#34;是你正在寻找的号码。作为奖励,您也可以获得时间信息,但是对于许多执行来说这会更准确。

答案 1 :(得分: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)

它读取包含要分析的函数的脚本的源,装饰这个,将所需的调用追加到末尾,执行它,将统计信息转储到字符串中,提取给定行号的命中数,以及将其作为int返回。它可以按要求运行,但会带来重要的性能损失。

也许更好的解决方案是删除分析器,但保持动态装饰和执行源代码的想法。如果植入它,我会编辑我的答案。

无论如何,谢谢Jason为我提供了一条出路!