当前位置: 首页 > 科技观察

Python性能分析百科

时间:2023-03-21 01:04:18 科技观察

尽管运行速度慢是Python的固有特性,但是大多数时候我们使用Python就意味着放弃对性能的追求。但是,即使是用纯Python完成同样的任务,老手写的代码可能比菜鸟写的代码块大几倍,甚至几十倍(这里不考虑算法因素,只考虑语言因素)).很多时候,我们把自己代码运行慢的原因归咎于python天生就慢,于是心安理得的放弃了深入探索。但是,真的是这样吗?面对python代码,你是否分析过以下问题:程序运行速度有多快?程序运行时间的瓶颈在哪里?你能做一些改进来提高运行速度吗?为了更好的理解python程序,我们需要一套工具,可以记录代码的运行时间,并生成性能分析报告,方便深入理解代码,从而进行有针对性的优化(本文重点介绍代码性能分析,而不是如何优化)。谁快谁慢假设有一个字符串,你想用字符'-'替换其中的空格,用python实现很简单,下面是四种解决方案:defslowest_replace():replace_list=[]fori,charinenumerate(orignal_str):c=charifchar!=""else"-"replace_list.append(c)return"".join(replace_list)defslow_replace():replace_str=""fori,charinenumerate(orignal_str):c=charifchar!=""else"-"replace_str+=creturnreplace_strdeffast_replace():return"-".join(orignal_str.split())deffastest_replace():returnorignal_str.replace("","-")这些的效率是多少四种方案,哪种方案比较慢??这是个问题!时间断点最直接的思路就是记录replace函数开始前的时间,记录程序结束后的时间,计算出时间差作为程序运行时间。Python提供了时间模块,time.clock()在Unix/Linux下返回CPU时间(以秒为单位,以浮点数表示),在Win下返回实时时间(Wall-clocktime),以秒为单位。由于替换函数的耗时可能很短,这里考虑执行10万次,然后检查不同函数的效率。我们的性能分析辅助函数如下:def_time_analyze_(func):fromtimeimportclockstart=clock()foriinrange(exec_times):func()finish=clock()print"{:<20}{:10.6}s".format(func.__name__+":",finish-start)这样你就可以明白上面程序的运行时间了:第一个解的时间是第四个解的45倍多,令人吃惊!同样是用python代码完成同样的功能,耗时差别很大。为了避免每次都在程序的开头和结尾插入时间断点,然后计算耗时,可以考虑实现一个上下文管理器,具体代码如下:classTimer(object):def__init__(self,verbose=False):self.verbose=详细def__enter__(self):self.start=clock()returnselfdef__exit__(self,*args):self.end=clock()self.secs=self.end-self.startself.msecs=self.secs*1000#millisecsifself.verbose使用:print'elapsedtime:%fms'%self.msecs时,只需要将测量时间的代码段放到with语句中即可,具体使用示例在要旨。在timeit上手动插入断点的方法非常原始,使用起来也不是那么方便,即使使用上下文管理器实现,也还是有些繁琐。幸运的是,Python提供了timeit模块来测试代码块的运行时间。它既提供了命令行界面又可以在代码文件中使用。CLI可以这样使用:$python-mtimeit-n1000000'"Iliketoreading.".replace("","-")'1000000loops,bestof3:0.253usecperloop$python-mtimeit-s'orignal_str="Iliketoreading."''"-".join(orignal_str.split())'1000000loops,bestof3:0.53usecperloop具体参数可以配合命令python-mtimeit-h查看帮助。常用的选项如下:-sS,--setup=S:用于初始化语句中的变量,只运行一次;-nN,--number=N:执行语句的次数,默认number会选择一个合适的;-rN,--repeat=N:重复测试的次数,默认3次;Python接口可以使用下面的程序来测试四个replace函数的运行(完整的测试程序可以在gist中找到):def_timeit_analyze_(func):fromtimeitimportTimert1=Timer("%s()"%func.__name__,"from__main__import%s"%func.__name__)print"{:<20}{:10.6}s".format(func.__name__+":",t1.timeit(exec_times))运行结果如下:Python的timeit提供timeit.Timer()类,该类构造方法如下:Timer(stmt='pass',setup='pass',timer=)其中:stmt:要计时的语句或函数;setup:import语句,为stmt语句搭建环境;timer:基于平台的时间函数(timerfunction);Timer()类有3个方法:timeit(number=1000000):返回stmt执行number次的秒数(float);repeat(repeat=3,number=1000000):repeat是重复整个测试的次数,number是执行stmt的次数,返回以秒为单位记录的每个测试周期的耗时列表;print_exc(file=None):打印stmt跟踪信息。此外,timeit还提供了另外三个函数方便使用,参数与Timer类似。timeit.timeit(stmt='pass',setup='pass',timer=,number=1000000)timeit.repeat(stmt='pass',setup='pass',timer=,repeat=3、number=1000000)timeit.default_timer()profile以上方法适用于比较简单的场合。在更复杂的情况下,可以使用标准库中的profile或cProfile,它可以统计程序中每个函数的运行时间,并提供可视化报表。大多数情况下,推荐使用cProfile,它是profile的C实现,适合长时间运行的程序。但是有些系统可能不支持cProfile,这时候我们就不得不使用profile了。您可以使用以下程序来测试timeit_profile()函数的运行时间分布。importcProfilefromtime_profileimport*cProfile.run("timeit_profile()")的输出可能很长。在很多情况下,我们可能只对花费最多时间的函数感兴趣。这时候先把cProfile的输出保存到诊断文件中,然后使用pstats自定义更好的输出(完整代码在gist上)。cProfile.run("timeit_profile()","timeit")p=pstats.Stats('timeit')p.sort_stats('time')p.print_stats(6)输出结果如下:如果觉得pstas使用不方便,还可以使用一些图形工具,比如gprof2dot,可以对cProfile的诊断结果进行可视化分析。vprofvprof也是一个很好的可视化工具,可以用来分析Python程序的运行时间。如下图所示:上面line_profiler的测试最多统计了函数的执行时间。很多时候我们想知道函数中每一行代码的执行效率。这时候就可以使用line_profiler了。line_profiler的使用特别简单,在需要监控的函数前面加上@profile装饰器即可。然后使用它提供的kernprof-l-v[source_code.py]行进行诊断。下面是一个简单的测试程序line_profile.py:fromtime_profileimportslow_replace,slowest_replaceforiinxrange(10000):slow_replace()slowest_replace()运行后结果如下:输出各列含义如下:Line#:行号Hits:当前行执行的次数。Time:当前行的执行时间,单位为“Timerunit:”PerHit:平均执行时间。%Time:当前行的执行时间占总时间的比例。LineContents:当前行的代码line_profiler估计的执行时间不是特别精确,但是可以用来分析当前函数中哪些行是瓶颈。