提升Python程序运行效率的方法总结

使用Python中的timeit函数测试程序运行效率:

def timeit(stmt="pass", setup="pass", timer=<built-in function perf_counter>,
           number=1000000, globals=None):
    """Convenience function to create Timer object and call timeit method."""
    return Timer(stmt, setup, timer, globals).timeit(number)
def repeat(stmt="pass", setup="pass", timer=<built-in function perf_counter>,
           repeat=5, number=1000000, globals=None):
    """Convenience function to create Timer object and call repeat method."""
    return Timer(stmt, setup, timer, globals).repeat(repeat, number)

从上面代码中可见,无论是timeit还是repeat都是先生成Timer对象,然后调用了Timer对象的timeit或repeat函数。
在使用timeit模块时,可以直接使用timeit.timeit()和tiemit.repeat(),还可以先用timeit.Timer()来生成一个Timer对象,然后再用TImer对象的timeit()和repeat()函数,后者更灵活一些。
函数的参数:
1、stmt:用于传入要测试时间的代码,可以直接接受字符串的表达式,也可以接受单个变量,还可以接受函数。传入函数时要把函数申明在当前文件中,然后在 stmt = ‘func()’ 执行函数,最后使用 setup = ‘from main import func’
2、setup:传入stmt的运行环境,比如stmt中使用到的参数、变量,要导入的模块等。可以写一行语句,也可以写多行语句,写多行语句时要用分号;隔开语句。
3、number:要测试的代码的运行次数,默认1000000次,对于耗时的代码,运行太多次会比较慢,此时建议自己修改一下运行次数
4、repeat:指测试要重复几次,每次的结果构成列表返回,默认5次。
一、直接使用timeit.timeit()、tiemit.repeat():

import timeit 
print(timeit.timeit(stmt= 'list(i**2 for i in normal_list)',setup = 'normal_list=range(10000)',number=10))
#0.3437936799875755
print(timeit.repeat(stmt= 'list(i**2 for i in normal_list)', setup='normal_list=range(10000)',repeat=2,number=10))
#[0.33649995761778984, 0.3394490767789293]
#setup 为复合语句
print(timeit.timeit(stmt= 'list(i**2 for i in normal_list)',setup = 'a=10000;normal_list=range(a)',number=10))
#0.33272367424748817
print(timeit.repeat(stmt= 'list(i**2 for i in normal_list)', setup='a=10000;normal_list=range(a)',repeat=2,number=10))
#[0.3323106610316342, 0.3356380911962764]
def func():
    normal_list=range(10000)
    L = [i**2 for i in normal_list]
#stmt为函数
print(timeit.timeit("func()", setup="from __main__ import func",number=10))
#0.12436874684622312
print(timeit.repeat("func()", setup="from __main__ import func",repeat=2,number=10))
#[0.12142133435126468, 0.12079555675148601]

直接用函数的方式速度更快。
二、先生成Timer,再调用timeit()、repeat():

import timeit 
#生成timer
timer1 = timeit.Timer(stmt= 'list(i**2 for i in normal_list)',setup = 'normal_list=range(10000)')
#调用timeit和repeat时还传number和repeat参数
print(timer1.timeit(number=10))
#0.34721554568091145
print(timer1.repeat(repeat=2,number=10))
#[0.3391925079630199, 0.34103400077255097]
#setup 为复合语句
timer1 = timeit.Timer(stmt= 'list(i**2 for i in normal_list)',setup = 'a=10000;normal_list=range(a)')
print(timer1.timeit(number=10))
0.34383463997592467
print(timer1.repeat(repeat=2,number=10))
#[0.34573984832288773, 0.34413273766891006]
#stmt为函数
def func():
    normal_list=range(10000)
    L = [i**2 for i in normal_list]
timer1 = timeit.Timer("func()", setup="from __main__ import func")
print(timer1.timeit(number=10))
#0.1223264363160359
print(timer1.repeat(repeat=2,number=10))
#[0.12266321844246209, 0.1264150395975001]

优化方法:
1、 使用函数,局部变量比全局变量快很多。尽量使用函数,如main()
2、 有选择性的消除属性访问。如多用from math import sqrt 而不要直接在程序中多次调用 math.sqrt(),或直接声明局部变量。

import math
def compute_roots(nums):
    sqrt = math.sqrt
    res = []
    res_append = res.append
    for n in nums:
        res_append(sqrt(n))
    return res

# 微优化
a = { i:i**2 for i in range(1000) } # 较快
b = [dict(i=i**2) for i in range(1000) ] #较慢

3、 避免不必要的抽象,如装饰器,@property等
4、 使用内建的字符串,元组,列表,集合,字典等容器
5、 避免不必要的数据结构或拷贝动作 
6、 使用cPython或pypy等。
7、 优化之前,先要跑起来,先要有正确的算法!低复杂度的算法远比程序的优化更重要。
8、 使用内置操作符。
Python是一种解释型语言,基于高级抽象。所以应该尽可能使用内置的。这将使代码更有效率,因为内置的预编译和快速。而包括解释步骤在内的漫长迭代很慢。
同样,喜欢使用内置功能,如地图,显着改善速度。
9、在循环中限制方法查找。
在循环中工作时,应该缓存方法调用,而不是在对象上调用它。否则,方法查找是昂贵的。


在这里插入图片描述

10、使用字符串进行优化。
字符串连接缓慢,不要在循环中执行。而是使用Python的join方法。或者,使用格式化功能来形成一个统一的字符串。
Python中的RegEx操作很快就被推回到C代码。然而,在某些情况下,基本的字符串方法(如<isalpha()/ isdigit()/ startswith()/ endswith()>更好地工作。
此外,您可以使用模块测试不同的方法。它将帮助您确定哪种方法是真正最快的。
11、用If语句进行优化。
Python像大多数编程语言一样懒惰。如果加入“AND”条件,那么并非所有条件都将被测试,以防其中一个变为假。
a、可以调整代码使用Python的行为。例如,要在列表中搜索固定模式,则可以通过添加以下条件来减小范围。
如果目标字符串的大小小于模式的长度,则添加一个“AND”条件,该条件变为false。
此外,可以先测试一个快速条件(如果有的话),如“string应该以@开头”或“string应该以点结尾”。
b、可以测试比< > 更快的条件< not >。例如: if done is None if done != None
12、 排序时使用键(key)
有很多老的Python排序代码,在创建一个自定义的排序时花费时间,但在运行时却能加速执行排序过程。元素排序的最好方法是尽可能使用键(key)和默认的sort()排序方法。例如:

import operator
somelist = [(1, 5, 8), (6, 2, 4), (9, 7, 5)]
somelist.sort(key=operator.itemgetter(0))
somelist
#Output = [(1, 5, 8), (6, 2, 4), (9, 7, 5)]
somelist.sort(key=operator.itemgetter(1))
somelist
#Output = [(6, 2, 4), (1, 5, 8), (9, 7, 5)]
somelist.sort(key=operator.itemgetter(2))
somelist
#Output = [(6, 2, 4), (9, 7, 5), (1, 5, 8)]

每一个实例中,根据选择作为key参数部分的索引,数组进行了排序。类似于利用数字进行排序,这种方法同样适用于利用字符串排序。
13、 优化循环
每种编程语言都会强调需要优化循环。当使用Python的时候,可以依靠大量的技巧使得循环运行得更快。然而经常漏掉的一个方法是:避免在一个循环中使用点操作。例如:

lowerlist = ['this', 'is', 'lowercase']
upper = str.upper
upperlist = []
append = upperlist.append
for word in lowerlist:
append(upper(word))
print(upperlist)
#Output = ['THIS', 'IS', 'LOWERCASE']

每次调用方法str.upper,Python都会求该方法的值。然而,如果用一个变量代替求得的值,值就变成了已知的,Python就可以更快地执行任务。优化循环的关键,是要减少Python在循环内部执行的工作量,因为Python原生的解释器在那种情况下,真的会减缓执行的速度。
(注意:优化循环的方法有很多,这只是其中的一个。例如,许多程序员都会说,列表推导即列表解析,是在循环中提高执行速度的最好方式(其实生成器表达式更快,能用就尽量用)。这里的关键是,优化循环是程序取得更高的执行速度的更好方式之一。)
14、 使用较新版本的Python
在网上搜索Python信息,都会发现无数人在问,从Python一个版本迁移到另一个版本的问题的信息。一般来说,Python的每一个版本都包含了能让其比上个版本运行更快的优化。版本迁移的限制因素是,你喜欢的那些库是否已经迁移到Python的较新版本。相比于询问是否应该进行版本迁移,关键问题是确定一个新版本什么时候有足够的支持,以保证迁移的可行性。
需要验证代码仍然运行。需要在Python的新版本下使用已获得的新库,然后检查应用程序是否需要重大改变。只有在作出必要的更正之后,才会注意到版本之间的差别。然而,如果正好应用程序能在新版本下运行,而不需要任何改变,则可能会错过那些版本升级带来的新特性。一旦进行了迁移,应该为新版本下的应用程序写一个说明,检查有问题的地方,并且优先考虑利用新版本的特性去更新那些地方。这样用户将会在升级的过程中更早的看到一个更大的性能提升。
15、 尝试多种编码方法
如果每次创建一个应用程序都是用相同的编码方法,几乎肯定会导致一些应用程序比它能够达到的运行效率慢的情况。作为分析过程的一部分,可以尝试一些实验。例如,在一个字典中管理一些元素,可以采用安全的方法确定元素是否已经存在并更新,或者可以直接添加元素,然后作为异常处理该元素不存在情况。考虑第一个编码的例子:

n = 16
myDict = {}
for i in range(0, n):
  char = 'abcd'[i%4]
  if char not in myDict:
    myDict[char] = 0
    myDict[char] += 1
    print(myDict)

这段代码通常会在myDict开始为空时运行得更快。然而,通常当mydict被数据填充(或者至少大部分被充填)时,另一种方法效果更好。

n = 16
myDict = {}
for i in range(0, n):
  char = 'abcd'[i%4]
  try:
    myDict[char] += 1
  except KeyError:
    myDict[char] = 1
  print(myDict)

两种情况下具有相同的输出:{‘d’: 4, ‘c’: 4, ‘b’: 4, ‘a’: 4}。唯一的不同是这个输出是如何得到的。跳出固定的思维模式,创造新的编码技巧,能够使应用程序获得更快的结果。
16、 交叉编译应用程序
开发者有时会忘记,电脑实际上是不懂任何用于创建现代应用程序的语言,电脑所能懂得是机器代码。为了能在电脑上运行应用程序,使用一个应用将人类可读的代码转换成计算机能理解的。有时候用一种语言,比如Python,写一个应用,并用另一种语言,比如C++,运行它,从性能的角度来看是有意义的。这取决于想要应用程序去做什么,以及主机系统可以提供的资源。
一个有趣的交叉编译器,Nuitka,可以将Python代码转换为C++代码。这么做的结果是,可以在原生模式下执行应用程序,而不是依靠解释器。根据平台和任务,可以看到一个显著的性能提升。
(注意:Nuitka目前还处于测试阶段,所以用它来产品程序时需要小心。实际上,目前最好将其用于实验。现在也有一些关于交叉编译是否是得到更好性能的最佳方式的讨论。开发者已经利用交叉编译好几年了,目的是实现特定的目标,比如更好的应用程序的速度。记住,每一个解决方案都会有得有失,应该在将一个解决方案用于生产环境之前就好好考虑一下得失情况。)
在使用一个交叉编译器时,要确保它支持你使用的Python的版本。Nuitka支持Python2.6、2.7、3.2和3.3。想让这个方案发挥作用,需要一个Python解释器和一个C++编译器。Nuitka支持多种C++编译器,包括Microsoft Visual Studio、MinGW 和 Clang/LLVM。
交叉编译也可能带来一些严重的负面影响。例如,当利用Nuitka工作时,会发现即使一个小程序也能消耗很大的硬盘空间,这是因为Nuitka使用大量的动态链接库(DLLs)实现Python的功能。所以面对一个资源有限的系统时,这个方案可能不会很好的起作用。

总结:
1、使用函数;
2、尽量不使用全局变量;
3、多用from math import sqrt而不要直接在程序中多次调用 math.sqrt();
4、尽量使用内建的字符串,元组,列表,集合,字典等容器;
5、变量缓存,避免多次计算,使之可重复调用;
6、使用if语句优化;
7、元素排序尽可能使用键(key)和默认的sort()排序方法;
8、优化循环:避免在循环中使用点操作(类似第5点,如:循环中每次调用方法str.upper,Python都会求该方法的值。如果用一个变量代替求得的值,值就变成已知的,可以更快地执行。),尽可能使用生成器表达式(速度最快)。

猜你喜欢

转载自blog.csdn.net/qq_38882327/article/details/89138425