Python代码优化

20

4
这篇内容最好发布在 http://codereview.stackexchange.com/ 上。 - Santa
或者你可以将其转化为一个关于优化Python代码的更一般性的问题,并提供想要优化的内容的简短示例。据我所知,目前还没有这样的问题? - James
如果这不是一个仅包含链接的问题,我可能会考虑重新打开它。 - Jonathan Mee
4个回答

108
如果你的问题是一般优化Python代码(我认为应该是这样的;),那么有各种有趣的事情可以做,但首先:你可能不应该过分优化Python代码!如果你正在尝试解决的问题使用了最快的算法,并且Python无法运行得足够快,那么你应该使用另一种语言。话虽如此,还有几种方法可以采用(因为有时候,你确实想让Python代码更快):

Profile (do this first!)

有很多方法可以对Python代码进行分析,但我将提到两种:cProfile(或profile)模块PyCallGraph

cProfile

这是你应该使用的,尽管解释结果可能有点令人生畏。它通过记录每个函数何时进入或退出以及调用函数是什么(并跟踪异常)来工作。

你可以像这样在cProfile中运行一个函数:

import cProfile
cProfile.run('myFunction()', 'myFunction.profile')

然后查看结果:
import pstats
stats = pstats.Stats('myFunction.profile')
stats.strip_dirs().sort_stats('time').print_stats()

这将向您展示程序中大部分时间花费在哪些函数中。

PyCallGraph

PyCallGraph 提供了一种最美观、最简单的方式来对 Python 程序进行分析,以便了解程序中时间的分配情况。然而,它会增加显著的执行开销。

运行 pycallgraph 的方法如下:

pycallgraph graphviz ./myprogram.py

简单!你将获得一个png图像作为输出(也许需要一段时间...)

使用库

如果你想在Python中实现某些功能,而已经存在一个模块(甚至是标准库中的),那么请使用该模块!

大多数标准库模块都是用C编写的,它们将比等效的Python实现(例如二分搜索)快上数百倍。

让解释器尽可能地完成你的工作

解释器会为你做一些事情,比如循环。真的吗?是的!你可以使用mapreducefilter关键字来显著加速紧密循环:

考虑:

for x in xrange(0, 100):
    doSomethingWithX(x)

vs:

map(doSomethingWithX, xrange(0,100))

显然,这会更快,因为解释器只需要处理单个语句而不是两个,但这有点模糊...事实上,这有两个原因更快:

  • 所有的流程控制(我们是否已经完成了循环等)在解释器中完成
  • doSomethingWithX函数名仅被解析一次

在for循环中,每次循环时Python都必须检查doSomethingWithX函数的确切位置!即使使用缓存也会有一定的开销。

请记住Python是一种解释性语言

(请注意,本节内容主要涉及微小的优化,您不应该让它们影响您的正常、易读的编码风格!) 如果您来自编译语言(像C或Fortran)的编程背景,则某些关于不同Python语句性能的事情可能会令人惊讶:

try:操作廉价,if操作昂贵

如果您有以下代码:

if somethingcrazy_happened:
     uhOhBetterDoSomething()
else:
     doWhatWeNormallyDo()

如果发生了什么疯狂的事情,doWhatWeNormallyDo() 将会抛出异常,那么将代码排列成这样会更快:

try:
    doWhatWeNormallyDo()
except SomethingCrazy:
    uhOhBetterDoSomething()

为什么?因为解释器可以直接开始执行你通常做的事情;而在第一种情况下,每次执行if语句时,解释器都必须进行符号查找,因为自上次执行该语句以来,名称可能指代不同的内容!(如果somethingcrazy_happened是全局的,名称查找可能会很困难。)

你是说谁??

由于名称查找的成本,将全局值缓存到函数中,并将简单的布尔测试内置到此类函数中,也可以更好地优化:

未优化的函数:

def foo():
    if condition_that_rarely_changes:
         doSomething()
    else:
         doSomethingElse()

优化方案是利用解释器已经在查找函数名称的事实,而不是使用变量!

当条件成立时:

foo = doSomething # now foo() calls doSomething()

当条件变为假时:

foo = doSomethingElse # now foo() calls doSomethingElse()

PyPy

PyPy是用Python编写的Python实现。这是否意味着它会无限慢地运行代码?其实不是这样的。PyPy实际上使用了即时编译器(JIT)来运行Python程序。

如果您不使用任何外部库(或者您使用的库与PyPy兼容),那么这是一种极为简单的方法,可以(几乎肯定地)加速程序中重复的任务。

基本上,JIT可以生成将执行Python解释器所需操作的代码,但速度要快得多,因为它是为单个情况生成的,而不必处理每个可能的合法Python表达式。

下一步该去哪里

当然,你应该首先优化你的算法和数据结构,考虑像缓存这样的东西,甚至是否需要在第一时间做这么多事情,但无论如何:

  • 这个页面提供了大量有关如何加速Python代码的信息,尽管其中一些已经过时了。

  • 这里是BDFL himself关于优化循环的主题。

从我的有限经验中,还有很多事情我没有提到,但是这个答案已经够长了!

这完全基于我的最近一些使用Python代码时,速度不够快的经验。我想再次强调,我并不认为我提出的任何建议都是好主意,但有时候你必须...

1
对于你的第一点加一:如果需要真正快速的话,用Fortran、C、C++或汇编语言编写代码,然后从Python中调用它。 - Zan Lynx
针对你的第一点,如果那是真的,那么你写的其他一切都没有意义。真正的情况是,优化Python代码只能让你达到一定的速度,超过这个速度你需要其他工具。因此,我会从以下几点开始你否则完美的答案:1)检查最佳算法 2)检查是否可以使用例如pypy 3)如果1或2让你接近所需的速度,则优化是可以的,否则选择不同的语言... - xubuntix
@xubuntix,我稍微修改了我的第一个观点。 - James
4
这篇文章似乎缺少了numpy。我的意思是,首先,大部分编程挑战都是掩盖在数学中的,并且第二,这基本上是让Python跑得更快的库。 - Voo
2
@Voo,我会将numpy作为使用正确库的一部分进行包含 - 当然,它是一个相当大的库,但它并不能做到所有事情;例如,它不太可能帮助您加速解析器。 - James

3
首先,对您的代码进行分析,以确定问题所在。有许多示例可用来完成此操作,以下是其中之一:https://codereview.stackexchange.com/questions/3393/im-trying-to-understand-how-to-make-my-application-more-efficient 您需要进行大量索引访问,例如:
for pair in range(i-1, j):
    if coordinates[pair][0] >= 0 and coordinates[pair][1] >= 0:

这句话可以更简单地写成:

for coord in coordinates[i-1:j]:
    if coord[0] >= 0 and cood[1] >= 0:

列表推导式很酷,也很“Pythonic”,但是如果您不创建4个列表,这段代码可能会更快地运行:

N = int(raw_input())
coordinates = []
coordinates = [raw_input() for i in xrange(N)]
coordinates = [pair.split(" ") for pair in coordinates]
coordinates = [[int(pair[0]), int(pair[1])] for pair in coordinates]

我建议将所有这些内容合并成一个简单的循环,或者如果你真的非常想使用列表推导式,可以把多重转换封装到一个函数中,该函数可操作raw_input()。

很不幸,for pair in range(i-1, j) 是重要的。我只想将循环应用于某些坐标部分。 - James Brewer
多次评估的好处很明显。 - James

2
这段文字的大意是: 这个答案展示了我如何找到需要优化的代码。假设有一行代码可以替换,而且它占用了40%的时间。那么它在调用堆栈中占据了40%的位置。如果你对调用堆栈进行10次采样,它将出现在其中的4次左右。实际上,它出现的次数并不重要。如果它出现了两次或更多,并且你可以替换它,那么你就可以节省它所占的任何时间。

我需要测试用例能够定位问题,但是InterviewStreet不会公开这些信息。有没有其他方法可以找出花费时间过长的地方? - James Brewer
@James Brewer 如果有一种方法可以编写自己的测试用例并对其进行分析,那就好了;) 我的意思是,无论如何,您都必须编写测试用例以确保代码正确(至少应该这样)- 这是一个很好的起点。 - Voo
@James:你不能运行这段代码吗?如果你没有数据集可以运行,那就模拟一个。 - Mike Dunlavey

1
大多数面试街道的问题似乎都是以一种验证您是否找到了具有正确大O复杂度的算法而不是您已经以最优方式编写解决方案的方式进行测试的。换句话说,如果由于时间不足而未通过某些测试用例,则问题很可能是您需要找出具有更低算法复杂度的解决方案,而不是微调您已有的算法。这就是为什么他们通常会说明N可能非常大的原因。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接