Python 是一种脚本语言,相比 C/C++ 这样的编译语言,在效率和性能方面存在一些不足。但是,有很多时候,Python 的效率并没有想象中的那么夸张。本文对一些 Python 代码加速运行的技巧N , m \ m进行整理。

0. 代码优# I o F + \ J化原则

本文会介绍不少的 Python 代码加速运行y + n # S y z的技巧。在深入代码优化细节之前,需要了解一些代码优化\ w ^ m T T基本原则。

h K ,一个基本原则是不要过早优化。很多\ 6 g人一开始写代码就奔着^ J % \ _ U性能优化的目标,“让正确的程序更快要比让快速的程序正确容易得\ X n多”。因此,优化的前提是代码能正常工作。过早地进行优化可能会. R 3 ` d )忽视对总体性能指标的把握,在得到全局结果前不要主次颠倒。

第二个基本原则是权衡优6 U p ;化的代价。优化是有代价的,想解决所有性能的问题是几乎不可能的。通常面临的选择是时间换空间或空间换时间。另外,开发代价也需要考虑。

第三个原则是不要优化那些无关紧要的部分。如果对代码的每一部分都b 3 – 3 – u去优化,这K r 5 P些修改会使代码x v b E L j难以阅读和理解。如果你的代码运行速度很慢,首先要找到代码运行慢的位置,通常是内部循环,专注于运行慢的地方进行优化。在其他地方,一点时间上的损失没有什么影响。

1. 避免全局变量

  1. #不推荐写法p u n 8 4 $ E )。代码耗时:26.8秒
  2. importmath
  3. size=10000
  4. forxinrange(size):
  5. foryinrange(size):
  6. z=math.sqrt(x)+matq / \ y ] 6h.sqrt(y)

许多[ F V程序员刚开始会用 Python 语言I d V @ ^写一些简单的脚本,当编写脚本时,通常习惯了直接将其写为全局变量,例如上面的代码。但是,由于全局变量和局部变量实现方式不同,g e D H 1 ~ 3 + S定义在全局范围内的代码运行速度会比定义在函数中的慢不少。通过将脚本语句放入到函数中,通常可带来 15%@ | K 6 ] – 30% 的速度提升。

  1. #推荐写法。代码耗时:20.6秒
  2. importmath
  3. defmain():#定义到函数中,以减少全部变量使用
  4. size=10000
  5. forxinranR 7 V E n oge(0 7 / e z R 1 { }size):
  6. foryinrange(size):
  7. z=math.sqrt(x)+math.sqrt(y)
  8. main()

2. 避免.

2.1 避免模块和函数属性访问

  1. #不推荐写法。代码耗时:14.5秒
  2. importmath
  3. defcomputeSq% G ]rt(size:int):
  4. resu3 R +lt=[]
  5. foriinrange(| } o ( ; M J qsF 6 j K Y Q $ 0 (ize):
  6. result.append(math.sqrt(i))
  7. returnresult
  8. defmain():
  9. size=10000
  10. for_^ ] ~ =inrange(size):
  11. result=computeSqrt(size)
  12. main()

每次使用.(属性访问操作符时)会触发特定的方法,如__getattribute__()和__getattr__(),这f Z S b /些方法会进行字典操作,因此会带N \ o p E #来额外的时间开销。通过from import语句,可以i s 5 Y r k (消除属性访问。

  1. #第一次优化写法。代码耗时:10.9秒
  2. frommathimportsqrt
  3. defcomputeSqrt(size:int):
  4. result=[]
  5. foriinrange(size):
  6. result.app2 Z ? E A r & v Jend(sqrt(i))S Z F T N D i#避免math.sqrt的使: j q J
  7. returnresult
  8. defmain():
  9. size=10000
  10. for_inrange(v Z } M msize):
  11. result=computeSqrt(size)
  12. maV 2 H a | E ~ + %in()

在第 1 节中我们讲到,局部变量的查找会比全局变量更快,因此对于频繁访问的变量sqrt,通过将其改为局部变量可以t O E加速运行。

  1. #第二次优化写法。代码耗时:9.9秒
  2. importmat: g [ v ` S m 5h
  3. defcomputeSqrt(siz6 c ^e:int):
  4. result=[]
  5. sqrt=math.sqrt#赋值给局部变量
  6. foriinrange(size; ! [ D):
  7. result.append(sqrm n = Vt(i))#避免math.sqrt的使用
  8. returnresult
  9. defmain():
  10. size=10000
  11. for_inrange(size):
  12. result=computeSqrt& F f W !(size)
  13. main()

除了mat# R t Q 3 X u w Nh.sqrt外,computeS5 o Z eqrt函数中还有.的存在,那就是调用| 4 Ylist的append方法。通过将该方法赋值给一个局部变量,可以彻底消除computeSqrt函数中for循环内部的.使用。

  1. #推荐写法。代码耗时:7.9秒
  2. importmath
  3. defcomp9 A $ YuteSZ 5 r = @ / {qrt(size:int):
  4. result=[]
  5. appA r y 5end=result.append
  6. sqrt=l c A ] d Pmath.sqrt#赋值给局部变量
  7. foriinrange(L r V : / 3size):
  8. append(sqrt(4 V ki))#避+ ] n g N免result.append和math.sqrt的使用
  9. returnresult
  10. defmain():
  11. size=10000
  12. for_inrange(size):
  13. result=computeSqrt(size)
  14. main()

2.2 避免类内属性访问

  1. #不推荐写法。代码耗时:10.4秒N : [
  2. importmath
  3. fromtypingimportLi6 X M z t I d &st
  4. classDemoClass:
  5. def__K l t @initC B 1 ) O__(self/ = L m T T { 7,value:int):
  6. self._value=value
  7. defcomputeSqr& ( : tt(self,size:int)->List[float]:
  8. result=[]
  9. append=result.append
  10. sqrt=u = 8math.sqrt
  11. for_inrange(size):
  12. appenc 3 m 6 S F =d(sqrt(self._value))
  13. returnresult
  14. defmain():
  15. size=10000
  16. for_inrange(size)z 4 Q E U + W - q:
  17. demo_instance=DemoClass(size)
  18. result=demo_instan2 @ = Lce.computeSqrt(size)
  19. main()

避免.的原则也适用于类内属性,访问self._value的速度会比访问一个局部变量更慢一些。通过将需要频繁访问的类内属性赋值给一个局部变量,可以提o $ T T升代码运行速度。

  1. #推荐写法。代码耗时:8.0秒
  2. importmath
  3. fromtypingi^ R :mportList
  4. classDemoClass:
  5. def__init__(self,value:int):
  6. self._value=value
  7. defcomputeSqrt(self,size:int)->List[float]:
  8. result=[]
  9. append=result.append
  10. sqrt=math.sqrt
  11. value=self._value
  12. for_inrange(size):
  13. append(sqrt(value))#避免self._value的使用
  14. return{ 3 D O W uresult
  15. defmain():
  16. size=10000
  17. for_inrange(size):
  18. demo_insu B v h D ? o }tance=DemoClass(size)
  19. demo_instance.computeSqrt(size)
  20. main()

3. 避J J L ~ 0 a免不必要的抽象

  1. #不推荐写法,代码耗时:0.55秒
  2. classDemoClass:
  3. def__init__(self,value:int):
  4. selfS Z L 4 P 7 j R 1.value=value
  5. @property
  6. defvalue(self)->int:
  7. returnself._value
  8. @valu} | r 8 |e.setter
  9. defvalue(self,x:int):
  10. self._value=x
  11. defmaim h = ] q c Xn():
  12. sizf A q 0e=1000000
  13. foriinrange(size):
  14. demo_instance=DemoClass(size)
  15. value=demo_instance.value
  16. demo_instanm _ M & L . _ ;ceq 4 Z p v \ M ~.value=i
  17. main()

任何时候当你使用额外的处理层Z 0 h 1 J p(比如装饰器、属性访问、描述器)去包装代码时,都会让代码变慢。大部分情况下,需要重新进行审视使用属性访问器的定义是否有必w , ] q要,使用getter/setter函数对属性进行访问通常是 C/C++ 程序员遗留下来的代码风格。如果真的没有必要,就使用简单属性。

  1. #推荐写法,代码耗时:0.M D z . i \ 1 / a33秒
  2. classDemoClass:
  3. def__init__(self,value:` w Q . 9 #int):
  4. self.value; ( 7=value#避免不必要的属性访问器
  5. defmain():
  6. size=1000000
  7. foriiT F i _ G 6 5 / UnrangeB ; F X M(size):
  8. demo_instance=X 4 o { DDemoClass(size)
  9. value=demo_instance.value
  10. demo_instance.value=i
  11. main()

4. 避免数据复制

4.1 避免无意义的数据复制

  1. #不推荐写法,代码耗时:6.5秒
  2. defmain():
  3. size=10000
  4. for_inp o ( T \range(size):
  5. value=range(size)
  6. value_list=[xforxinvalue]
  7. squar. 3 9 I 2 H [e_list=[x*xforxinvalue_list]
  8. main()

上面的代+ \ t u $码中value_list完全没有必要,这会创建不必要的数据结构或复制。

  1. #推荐写法,代码耗时:4.8秒
  2. deft I * imain():
  3. size=1- A r ( n0000
  4. for_iI y 0 K f xnrange(size):
  5. value=range(size)
  6. square_list=[x*xforxinvalue]#避免无意义的复制
  7. main()

另外一种情况是对 Python 的数据共享机制过于偏执,并没有很好地理解或信任 Python 的内P q : h 2 G存模型M 8 6 3 f K i d a,滥用 copy.deepcopy()之类的函数。通常在这些代码中是可以去掉复制操作的。5 A T

4.2 交换值时不使用中间变量

  1. #不推荐写法,代码耗时:0.07秒
  2. defmain():
  3. size=1000000
  4. for_inrange(size):
  5. a=3
  6. b=5
  7. temp=a
  8. a=b
  9. b=temp
  10. main()

上面的w \ 9 0 p代码在交换值时创建了一个临时变量temp,如果不借助中间变量,代码更为简洁、且运行速度更快。w Q ~ 7 q F * E

  1. #推荐写法,代码耗时:0.06秒
  2. defmain():
  3. size=1000000
  4. foN d 2 I Xr_inra\ n 9 g \ Ange(size):
  5. a=3
  6. b=5
  7. a,bb=b,a#不j : J n ` U H p借助中间变量
  8. main()

4.3 字符串拼接用joi* = Q X k + \n而不是+

  1. #不推荐写法,代码耗时:2.6秒
  2. importstring
  3. fromtypiP + zngimportList
  4. defconcatSe q H Mtring(string_list:List[str])->str:
  5. rS ; s .esult=''
  6. forstr_iinstring_liU v ost:
  7. res5 A h x N * g gult+=str_i
  8. returnresult
  9. defmain():
  10. string_list=list(string.ascii_letters*100)
  11. for_inrange(10000):~ m l U X X k s
  12. result=concatString(striC \ { 1 { f n xng_list)
  13. main()

当使用a + b拼接字符串时[ 5 l w m 2,由于 Python 中字符串是不可变对象,其会申请一块内存空间,将a和b分别复制到该新申请的内存空间中。8 C @ ? 1 # y w l因此,如果要拼接 n 个字符串,会产生 n-1 个中间结果,每产生一个中间结果都需要申请和复制一| T ? { s k次内存,严重影响运行效率。而使用jo) X T s X m sin()拼接字符串时,会首先计算出需要申请的总的内存空间,然后一次性地申请所需内存,并将每个字符串元素复制到该内存中去。

  1. #推荐写法,代码耗时:0.3秒
  2. importstring
  3. fromtypingimportList
  4. defconcatStrinT ] q )g(string_Z M q M e 6 9l~ 4 `ist:List[str])->str:
  5. return''.join(string_list)#使用join而不是+
  6. deO = E / V 3fmain():
  7. strinP ; X ~ P u / [ }g_list=list(string.ascii_letters*100)
  8. for_inrange(10000):
  9. resf g { ;ult=concatString(string_list)
  10. main()

5. 利用ifE = \ V e O x条件的短路特性

  1. #不推荐写法,代码耗时:0.05秒
  2. fromtypie f ingi) T ? 4 w W [ 1mportList
  3. defconcatString(string_list:List[str])->str:
  4. abbreviations={'cf.','O o je.g.','e# 5 = j Q g Jx.','etc.','flg# ( { 3 { o X.','i.e.','Mr.','vs.'}
  5. ab& O z P a 9 7 tbr_count=0
  6. result=''
  7. forst] 0 ! S &r_iinstring_liS j ^ 2 B d .st:
  8. ifstr_iinabbreviations:
  9. result+=str_i
  10. returnresult
  11. deH \ `fmaiX } } W 4 V w v ln():
  12. fR W k o 7 9or_inrange(10000):
  13. st? @ F v h f z zring_list=['Mr.','Hat','is','Chasing','the','black','cat','.']
  14. result=con5 R & ! ` zcatString(string_list)
  15. main()

if 条件的短路特性是指对if a and b这样的语句, 当a为False时将直接返回,不再计算b;对于if a or b这样? [ & i . 2 8 +的语句,当a为True时将直接返回,不再计算b。因此, 为了节约运行时间,对于or语句,应该将值为True可能性比较高的变量写在or前,而and应该推后。

  1. #推荐写法,代码耗时:0.03秒
  2. fromtypingimportList
  3. defconcatString(s w $ k F 3 0 2 jstring_list:Li. O ) M W 9 ; [ 6st! N w w w s G[str])->stN ) J \ G : Xr:
  4. abbreviations={'cf.'A \ H,'e.g.','ex.','etc.','flg.','i.e.','Mr.','q D y v $ F Uvs.'}
  5. abbr_count=0
  6. result=''
  7. forstr_iinstring_list:
  8. ifstr_i[-1]=='.'andstr_ii0 I | - c i 5nabbreviations! _ S n:#利用if条件的短路特性
  9. result+=str_i
  10. returnresult
  11. defmain(0 ^ Z F):
  12. for_inrange(10000):
  13. string_list=['Mr.','Hat','is','Chasing','the','black','cat','.']
  14. result=concatString2 M K(string_list)
  15. main()

6. 循环优化j { { 7 W *

6.1 用for循环代替whi) 7 } ,le6 \ : h _ } t y G循环

  1. #不推荐写法。代码耗时:6.7秒
  2. defcompH I ; ` ; z EuteSum(size:int)->int:
  3. sum_=0
  4. i=0
  5. whilA s . 9ei<size:
  6. sum_+=i
  7. i+=1
  8. returnsum_
  9. defmain():
  10. size=10000
  11. for_inrange(size):
  12. sp v | yum_=computeSuQ o h \ | N _m(size)
  13. main()

Python 的for循环比while循环快不少。

  1. #推荐写法。代码耗时:4.3秒
  2. defcomputeSum(size:int)->int:
  3. sum_=0
  4. foriinrange(size):#for循环代替while循环
  5. sum_+=i
  6. returnsum_
  7. defmainU / . $ p ) Q G 0():
  8. size=10000
  9. for_inra) 0 7 F d s {nge(size):
  10. sum_=computw # _eSum(size)
  11. main()

6.2 使用隐式for循环代替显式for循环

针对上面的例子,更进一步可以用隐式for循环来替代显4 o ( _ I N ; t式for循环

  1. #推荐写法。代码耗时:1.7秒
  2. defcomputeSum(size:int)->int:
  3. returnsum(range(size))#隐式for循环代替显式for\ 2 a ~ T d循环
  4. defmain():
  5. size=10000
  6. for_inrange(size):
  7. sum=computeSum(size)
  8. main()

6.3 减少内层f4 % : o 3 k Eor循环的计算

  1. #不推荐写法。代码耗D \ 7 @ 8 r v k W时:12.8秒
  2. importmath
  3. defmain():
  4. size=10000
  5. sqrt=math.sqrt
  6. fo_ c o d 6 ( 7 yrxinrange(size):
  7. foryinrange(size):
  8. z=sqrt(x)+u 6 o & W Xsqrt(y)
  9. main()

上面的代码中sqrt(x)位于内侧for循环, 每次训练过程中都会重新计算一次,增加了时= } C 0间开销。

  1. #推荐写法。代码耗时:7.0秒
  2. importmath
  3. defmain():
  4. size=10000
  5. sqrt=math.sqrt
  6. forxinrange(size):
  7. sqrtsqrt_x=sqrt(x)#减少内层for循环的计算
  8. foryinrange(size):
  9. z=sqrt_x+sqrt(y)
  10. main()

7. 使用numba.jit

我们沿用上面介绍过的例子,在此基础上使用numba.jit。numba可以将 Python 函数 JIT 编译为机器码执行,大大提高代码运行速度。关r Q A J X x h于numba的更多信息见下面的主页:http://numba.pydata.org/numba.pydata.org

  1. #推荐写法。代码耗时:0.62秒
  2. importy a v 6 = [numba
  3. @numba.jit
  4. defcomput2 h $ q = 2 \eSum(size:float)->int:
  5. sum=0
  6. foriin: b A I R W granger N C \(size):
  7. sum+=i
  8. retux ! , Yrnsum
  9. def! w ~ cmain():
  10. s0 J #ize=10000
  11. for_inrange(sizx { \ q ! Re):
  12. sum=computeSum(size)
  13. main()

8. 选择合适的数据结构

Python 内置的数据结构如sQ S V L 2 Z C @tr, tuple, list, set, dict底层都是 C 实现的,速度非常快,自己实现3 } 1 $新的数据结构想在性能上达到内置的速度几乎是不可能的。

list类似于 C++ 中的std::vector,是一种动态数组。其会预分配一定内存空间,当预分配的1 4 . O .内存空间用完,又继续向其中添加元素时,会申请一块更大的内Z 9 V M I R O存空间,然后将原有的所有元素都复制过去,之后销毁之前的内存b H _ O { : N P空间,^ \ t ^ U再插入6 ~ W |新元素。

删除元素时操作类似,当已使用内存空间比预分配内存空间的一半还少时,V A , a Z6 2 \ G (另外申请一块小内存,做一次元素复制,之后销毁原有大内存空间Y ^ z R } ^

因此,如果有频繁的新增、删除操作,; V { W J ] 6 t新增、删除的元素数量又很多时,list的效率不高。此时,应该考虑使用collections.deque。collections.deque是双端队列,同时具备栈和队列的特性,能够在两端进行 O(1) 复杂度的插入和删除操作。

list的查找操作也非常耗时。当T ? z ] O P ; T需要在list频繁查找某些元素,或频繁有序访问这些元素时,可以使用bisect维护list对象有序并在其中x B ) l * H \进行二分查找,提升查找的效率。

J + @ P ) 2 ( q C外一个常见需求是查找极小值或极大值,此时可以使用heapq模块将list转化为一个堆,使得获取最小值的时间复杂度是 O(1)。

【责h ~ e ] / T _ @ Z任编辑:庞桂玉 TEL:(010)68476606】

7 e g m w b0

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注