关于python:一个装饰器,它描述一个方法调用并记录分析结果

A decorator that profiles a method call and logs the profiling result

我想创建一个描述方法并记录结果的装饰器。怎么能做到?


如果需要正确的分析而不是计时,可以使用cProfile的未记录功能(从这个问题开始):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cProfile

def profileit(func):
    def wrapper(*args, **kwargs):
        datafn = func.__name__ +".profile" # Name the data file sensibly
        prof = cProfile.Profile()
        retval = prof.runcall(func, *args, **kwargs)
        prof.dump_stats(datafn)
        return retval

    return wrapper

@profileit
def function_you_want_to_profile(...)
    ...

如果希望对文件名进行更多控制,则需要另一层间接寻址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import cProfile

def profileit(name):
    def inner(func):
        def wrapper(*args, **kwargs):
            prof = cProfile.Profile()
            retval = prof.runcall(func, *args, **kwargs)
            # Note use of name from outer scope
            prof.dump_stats(name)
            return retval
        return wrapper
    return inner

@profileit("profile_for_func1_001")
def func1(...)
    ...

它看起来很复杂,但是如果您一步一步地遵循它(并注意调用探查器的区别),它应该会变得清晰。


装饰师看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time
import logging

def profile(func):
    def wrap(*args, **kwargs):
        started_at = time.time()
        result = func(*args, **kwargs)
        logging.info(time.time() - started_at)
        return result

    return wrap

@profile
def foo():
    pass

不管怎样,如果你想做一些严肃的分析,我建议你使用概要文件或cprofile包。


如果您已经了解了如何为cprofile编写装饰器,请考虑使用functools.wrapps。

只需添加一行就可以帮助您更容易地调试装饰器。如果不使用functools.wrapps,修饰函数的名称将为"wrapper",并且的docstring将丢失。

所以改进后的版本是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import cProfile
import functools

def profileit(func):
    @functools.wraps(func)  # <-- Changes here.
    def wrapper(*args, **kwargs):
        datafn = func.__name__ +".profile" # Name the data file sensibly
        prof = cProfile.Profile()
        retval = prof.runcall(func, *args, **kwargs)
        prof.dump_stats(datafn)
        return retval

    return wrapper

@profileit
def function_you_want_to_profile(...)
    ...

这里是一个具有两个参数的修饰器,配置文件输出的文件名和按结果排序的字段。默认值是累积时间,这对于发现瓶颈很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def profileit(prof_fname, sort_field='cumtime'):
   """
    Parameters
    ----------
    prof_fname
        profile output file name
    sort_field
       "calls"     : (((1,-1),              ),"call count"),
       "ncalls"    : (((1,-1),              ),"call count"),
       "cumtime"   : (((3,-1),              ),"cumulative time"),
       "cumulative": (((3,-1),              ),"cumulative time"),
       "file"      : (((4, 1),              ),"file name"),
       "filename"  : (((4, 1),              ),"file name"),
       "line"      : (((5, 1),              ),"line number"),
       "module"    : (((4, 1),              ),"file name"),
       "name"      : (((6, 1),              ),"function name"),
       "nfl"       : (((6, 1),(4, 1),(5, 1),),"name/file/line"),
       "pcalls"    : (((0,-1),              ),"primitive call count"),
       "stdname"   : (((7, 1),              ),"standard name"),
       "time"      : (((2,-1),              ),"internal time"),
       "tottime"   : (((2,-1),              ),"internal time"),
    Returns
    -------
    None

   """

    def actual_profileit(func):
        def wrapper(*args, **kwargs):
            prof = cProfile.Profile()
            retval = prof.runcall(func, *args, **kwargs)
            stat_fname = '{}.stat'.format(prof_fname)
            prof.dump_stats(prof_fname)
            print_profiler(prof_fname, stat_fname, sort_field)
            print('dump stat in {}'.format(stat_fname))
            return retval
        return wrapper
    return actual_profileit


def print_profiler(profile_input_fname, profile_output_fname, sort_field='cumtime'):
    import pstats
    with open(profile_output_fname, 'w') as f:
        stats = pstats.Stats(profile_input_fname, stream=f)
        stats.sort_stats(sort_field)
        stats.print_stats()

我喜欢@detly的回答。但有时使用snakeviz查看结果是一个问题。

我做了一个稍微不同的版本,将结果作为文本写入同一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cProfile, pstats, io

def profileit(func):
    def wrapper(*args, **kwargs):
        datafn = func.__name__ +".profile" # Name the data file sensibly
        prof = cProfile.Profile()
        retval = prof.runcall(func, *args, **kwargs)
        s = io.StringIO()
        sortby = 'cumulative'
        ps = pstats.Stats(prof, stream=s).sort_stats(sortby)
        ps.print_stats()
        with open(datafn, 'w') as perf_file:
            perf_file.write(s.getvalue())
        return retval

    return wrapper

@profileit
def function_you_want_to_profile(...)
    ...

我希望这能帮助别人…