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

7 投票
2 回答
3100 浏览
提问于 2025-04-18 17:08

问题

为了教学目的,我想要计算在一个特定的函数中,某一行被执行的次数,但不想修改或装饰这个函数。比如,对于下面这个函数:

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”就是你要找的数字。作为额外的好处,你还可以得到一些时间信息,不过如果执行多次,时间信息会更准确。

撰写回答