如何进行 Python性能分析,你才能如鱼得水?
【编者按】本文作者为 Bryan Helmig,主要介绍 Python 应用性能分析的三种进阶方案。文章系国内 ITOM 管理平台 OneAPM 编译呈现。
我们应该忽略一些微小的效率提升,几乎在 97% 的情况下,都是如此:过早的优化是万恶之源。—— Donald Knuth
如果不先想想Knuth的这句名言,就开始进行优化工作,是不明智的。然而,有时你为了获得某些特性不假思索就写下了O(N^2) 这样的代码,虽然你很快就忘记它们了,它们却可能反咬你一口,给你带来麻烦:本文就是为这种情况而准备的。
本文会介绍用于快速分析Python程序的一些有用工具和模式。主要目标很简单:尽快发现问题,修复问题,并确认修复是行之有效的。
编写一个测试
在教程开始前,要先写一个简单的概要测试来演示延迟。你可能需要引入一些最小数据集来重现可观的延迟。通常一或两秒的运行时间,已经足够在发现问题时,让你做出改进了。
此外,进行一些基础测试来确保你的优化不会修改缓慢代码的行为也是有必要的。在分析和调整时,你也可以多次运行这些测试,作为基准。
那么现在,我们来看第一个分析工具。
简单的计时器
计时器是简单、灵活的记录执行时间的方法。你可以把它放到任何地方,并且几乎没有副作用。自己创建计时器非常简单,并且可以根据你的喜好定制化。例如,一个简单的计时器可以这么写:
import time
def timefunc(f):
def f_timer(*args, **kwargs):
start = time.time()
result = f(*args, **kwargs)
end = time.time()
print f.__name__, 'took', end - start, 'time'
return result
return f_timer
def get_number():
for x in xrange(5000000):
yield x
@timefunc
def expensive_function():
for x in get_number():
i = x ^ x ^ x
return 'some result!'
# prints "expensive_function took 0.72583088875 seconds"
result = expensive_function()
当然,你可以用上下文管理器来增强它的功能,添加一些检查点或其他小功能:
import time
class timewith():
def __init__(self, name=''):
self.name = name
self.start = time.time()
@property
def elapsed(self):
return time.time() - self.start
def checkpoint(self, name=''):
print '{timer} {checkpoint} took {elapsed} seconds'.format(
timer=self.name,
checkpoint=name,
elapsed=self.elapsed,
).strip()
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.checkpoint('finished')
pass
def get_number():
for x in xrange(5000000):
yield x
def expensive_function():
for x in get_number():
i = x ^ x ^ x
return 'some result!'
# prints something like:
# fancy thing done with something took 0.582462072372 seconds
# fancy thing done with something else took 1.75355315208 seconds
# fancy thing finished took 1.7535982132 seconds
with timewith('fancy thing') as timer:
expensive_function()
timer.checkpoint('done with something')
expensive_function()
expensive_function()
timer.checkpoint('done with something else')
# or directly
timer = timewith('fancy thing')
expensive_function()
timer.checkpoint('done with something')
有了计时器,你还需要进行一些“挖掘”工作。 封装一些更为高级的函数,然后确定问题根源之所在,进而深入可疑的函数,不断重复。当你发现运行特别缓慢的代码之后,修复它,然后进行测试以确认修复成功。
提示:不要忘了便捷的 timeit 模块!将它用于小段代码块的基准校验比实际测试更加有用。
- 计时器的优点:容易理解和实施,也非常容易在修改前后进行对比,对于很多语言都适用。
- 计时器的缺点:有时候,对于非常复杂的代码库而已太过简单,你可能会花更多的时间创建、替换样板代码,而不是修复问题!
内建分析器
内建分析器就好像大型枪械。虽然非常强大,但是有点不太好用,有时,解释和操作起来比较困难。
你可以点此阅读更多关于内建分析模块的内容,但是内建分析器的基本操作非常简单:你启用和禁用分析器,它能记录所有的函数调用和执行时间。接着,它能为你编译和打印输出。一个简单的分析器用例如下:
import cProfile
def do_cprofile(func):
def profiled_func(*args, **kwargs):
profile = cProfile.Profile()
try:
profile.enable()
result = func(*args, **kwargs)
profile.disable()
return result
finally:
profile.print_stats()
return profiled_func
def get_number():
for x in xrange(5000000):
yield x
@do_cprofile
def expensive_function():
for x in get_number():
i = x ^ x ^ x
return 'some result!'
# perform profiling
result = expensive_function()
在上面代码中,控制台打印的内容如下:
5000003 function calls in 1.626 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
5000001 0.571 0.000 0.571 0.000 timers.py:92(get_number)
1 1.055 1.055 1.626 1.626 timers.py:96(expensive_function)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
如你所见,它给出了不同函数调用的详细数据。但是,它遗漏了一项关键信息:是什么原因,导致函数运行如此缓慢?
然而,这对于基础分析来说是个好的开端。有时,能够帮你尽快找到解决方案。我经常在开始调试过程时,把它作为基本测试,然后再深入测试某个不是运行缓慢,就是调用频繁的特定函数。
- 内建分析器的优点:没有外部依赖,运行非常快。对于快速的概要测试非常有用。
- 内建分析器的缺点:信息相对有限,需要进一步的调试;报告不太直观,尤其是对于复杂的代码库。
Line Profiler
如果内建分析器是大型枪械,line profiler就好比是离子炮。它非常的重量级且强大,使用起来也非常有趣。
在这个例子里,我们会用非常棒的kernprof line-profiler,作为 line_profiler PyPi包。为了方便使用,我们会再次用装饰器进行封装,同时也可以防止我们把它留在生产代码里(因为它比蜗牛还慢)。
try:
from line_profiler import LineProfiler
def do_profile(follow=[]):
def inner(func):
def profiled_func(*args, **kwargs):
try:
profiler = LineProfiler()
profiler.add_function(func)
for f in follow:
profiler.add_function(f)
profiler.enable_by_count()
return func(*args, **kwargs)
finally:
profiler.print_stats()
return profiled_func
return inner
except ImportError:
def do_profile(follow=[]):
"Helpful if you accidentally leave in production!"
def inner(func):
def nothing(*args, **kwargs):
return func(*args, **kwargs)
return nothing
return inner
def get_number():
for x in xrange(5000000):
yield x
@do_profile(follow=[get_number])
def expensive_function():
for x in get_number():
i = x ^ x ^ x
return 'some result!'
result = expensive_function()
如果运行上面的代码,就会看到以下的报告:
Timer unit: 1e-06 s
File: test.py
Function: get_number at line 43Total time: 4.44195 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
43 def get_number():
44 5000001 2223313 0.4 50.1 for x in xrange(5000000):
45 5000000 2218638 0.4 49.9 yield x
File: test.py
Function: expensive_function at line 47Total time: 16.828 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
47 def expensive_function():
48 5000001 14090530 2.8 83.7 for x in get_number():
49 5000000 2737480 0.5 16.3 i = x ^ x ^ x
50 1 0 0.0 0.0 return 'some result!'
如你所见,这是一个非常详细的报告,能让你完全洞悉代码的运行情况。和内建的cProfiler不同,它能分析核心语言特性的耗时,比如循环或导入,并且给出不同代码行的耗时累计值。
这些细节能让我们更容易理解函数内部原理。 此外,如果需要研究第三方库,你可以将其导入,直接输到装饰器中。
提示:将测试函数封装为装饰器,再将问题函数作为参数传进去就好了!
- Line Profiler 的优点:有非常直接和详细的报告。能够追踪第三方库里的函数。
- Line Profiler 的缺点:因为系统开销巨大,会比实际执行时间慢一个数量级,所以不要用它进行基准测试。同时,它是外部工具。
结论和最佳方案
你应该使用简单的工具(比如计时器或内建分析器)对测试用例(特别是那些你非常熟悉的代码)进行基本检查,然后使用更慢但更加细致的工具,比如 line_profiler
,深入检查函数内部。
十有八九,你会发现一个愚蠢的错误,比如在循环内重复调用,或是使用了错误的数据结构,消耗了90%的函数执行时间。在进行快速(且令人满意的)调整之后,问题就能得到解决。
如果你仍然觉得程序运行太过缓慢,然后开始进行对比属性访问(ttribute accessing)方法,或调整相等检查(equality checking)方法等晦涩的调整,你可能已经适得其反了。你应该考虑如下方法:
1.忍受缓慢或者预先计算/缓存
2.重新思考整个实施方法
3.使用更多的优化数据结构(通过 Numpy,Pandas等)
4.编写一个 C扩展
注意,优化代码会带来有罪恶感的快乐!寻找加速Python的合理方法很有趣,但是不要因为加速,破坏了本身的逻辑。易读的代码比运行速度更重要。实施缓存,往往是最简单的解决方法。
教程到此为止,希望你今后的Python性能分析能够如鱼得水!
PS: 点此查看代码实例。此外,点此学习如何如鱼得水地调试 Python 程序。
OneAPM 能帮你查看 Python 应用程序的方方面面,不仅能够监控终端的用户体验,还能监控服务器性能,同时还支持追踪数据库、第三方 API 和 Web 服务器的各种问题。想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客
原文地址:https://zapier.com/engineering/profiling-python-boss/
如何进行 Python性能分析,你才能如鱼得水?的更多相关文章
- Python性能分析
Python性能分析 https://www.cnblogs.com/lrysjtu/p/5651816.html https://www.cnblogs.com/cbscan/articles/33 ...
- python性能分析(一)——使用timeit给你的程序打个表吧
前言 我们可以通过查看程序核心算法的代码,得知核心算法的渐进上界或者下界,从而大概估计出程序在运行时的效率,但是这并不够直观,也不一定十分靠谱(在整体程序中仍有一些不可忽略的运行细节在估计时被忽略了) ...
- Python性能分析工具Profile
Python性能分析工具Profile 代码优化的前提是需要了解性能瓶颈在什么地方,程序运行的主要时间是消耗在哪里,对于比较复杂的代码可以借助一些工具来定位,python 内置了丰富的性能分析工具,如 ...
- Python性能分析与优化PDF高清完整版免费下载|百度云盘
百度云盘|Python性能分析与优化PDF高清完整版免费下载 提取码:ubjt 内容简介 全面掌握Python代码性能分析和优化方法,消除性能瓶颈,迅速改善程序性能! 对于Python程序员来说,仅仅 ...
- 如何进行python性能分析?
在分析python代码性能瓶颈,但又不想修改源代码的时候,ipython shell以及第三方库提供了很多扩展工具,可以不用在代码里面加上统计性能的装饰器,也能很方便直观的分析代码性能.下面以我自己实 ...
- Python性能分析指南
http://www.admin10000.com/document/2861.html 尽管并非每个你写的Python程序都需要严格的性能分析,但了解一下Python的生态系统中很多优秀的在你需要做 ...
- Python丨Python 性能分析大全
虽然运行速度慢是 Python 与生俱来的特点,大多数时候我们用 Python 就意味着放弃对性能的追求.但是,就算是用纯 Python 完成同一个任务,老手写出来的代码可能会比菜鸟写的代码块几倍,甚 ...
- Python性能分析指南(未完成)
英文原文:http://www.huyng.com/posts/python-performance-analysis/ 译文:http://www.oschina.net/translate/pyt ...
- Python—— 性能分析入门指南
虽然并非你编写的每个 Python 程序都要求一个严格的性能分析,但是让人放心的是,当问题发生的时候,Python 生态圈有各种各样的工具可以处理这类问题. 分析程序的性能可以归结为回答四个基本问题: ...
随机推荐
- python中使用eval() 和 ast.literal_eval()的区别 分类: Python 2015-05-11 15:21 1216人阅读 评论(0) 收藏
eval函数在python中做数据类型的转换还是很有用的.它的作用就是把数据还原成它本身或者是能够转化成的数据类型. 那么eval和ast.literal_val()的区别是什么呢? eval在做计算 ...
- Python -- 网络编程 -- 抓取网页图片 -- 豆瓣妹子
首先分析页面URL,形如http://dbmeizi.com/category/[1-14]?p=[0-476] 图片种类对应编号: 1:'性感', 2:'有沟', 3:'美腿', 4:'小露点', ...
- #pragma的一些用法
1.#pragma message message 参数:Message参数能够在编译信息输出窗口输出相应的信息,这对于源代码的信息控制特别重要,其使用方法为: #pragma message(&qu ...
- MySQL查询时区分大小写
在创建MySQL数据库时,下面这些参数可供我们选择:*_bin: 表示的是binary case sensitive collation,也就是说是区分大小写的 *_cs: case sensitiv ...
- paxos协议更新日志
基于Paxos协议的数据同步与传统主备方式最大的区别在与Paxos只需任意超过半数的副本在线且相互通信正常,就可以保证服务的持续可用,且数据不丢失. Basic paxos协议更新日志 我们将数据持久 ...
- Nodejs学习笔记(三)—模块
简介及资料 通过Node.js的官方API可以看到Node.js本身提供了很多核心模块 http://nodejs.org/api/ ,这些核心模块被编译成二进制文件,可以require('模块名') ...
- spring-boot前端参数单位转换
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import ja ...
- msdn 中MethodBase.Invoke 方法 介绍中的坑
模块开发总结: c#动态调用webservices 来自网络及使用心得. msdn: MethodBase.Invoke 方法 (Object, Object[]) 使用指定的参数调用当前实例所表示的 ...
- java SE 入门之运算符(第三篇)
在 java 中主要有以上运算符,分为 赋值运算符 ,算术运算符.关系运算符(比较运算符),逻辑运算符和三目运算符(三元运算符). 我们一起来学习下吧! 一.赋值运算符 赋值运算符主要有五个.分别是 ...
- 通过set赋值,与select赋值的区别
---通过set赋值,与select赋值的区别.declare @a int--set @a=(select count(*) from TblStudent)select @a=count(*) f ...