
Python 是一种脚本语言,相比 C/C++ 这样的编译语言,在效率和性能方面存在一些不足。但是,有很多时候,Python 的效率并没有想象中的那么夸张。本文对一些 Python 代码加速运行的技巧进行整理。
![]() % ^5 M8 E$ E0 Q' j- M3 Y8 E' c 0. 代码优化原则 本文会介绍不少的 Python 代码加速运行的技巧。在深入代码优化细节之前,需要了解一些代码优化基本原则。 第一个基本原则是不要过早优化。很多人一开始写代码就奔着性能优化的目标,“让正确的程序更快要比让快速的程序正确容易得多”。因此,优化的前提是代码能正常工作。过早地进行优化可能会忽视对总体性能指标的把握,在得到全局结果前不要主次颠倒。 第二个基本原则是权衡优化的代价。优化是有代价的,想解决所有性能的问题是几乎不可能的。通常面临的选择是时间换空间或空间换时间。另外,开发代价也需要考虑。 第三个原则是不要优化那些无关紧要的部分。如果对代码的每一部分都去优化,这些修改会使代码难以阅读和理解。如果你的代码运行速度很慢,首先要找到代码运行慢的位置,通常是内部循环,专注于运行慢的地方进行优化。在其他地方,一点时间上的损失没有什么影响。 $ z* K& u+ J* _" I 1. 避免全局变量# 不推荐写法。代码耗时:26.8秒! k8 H" f& T+ x# x8 A) H import math9 P' D* W3 }# G p* _7 A size = 100001 E$ G7 L0 c7 l s8 E for x in range(size): for y in range(size):7 P6 S D& G6 E' Y z = math.sqrt(x) + math.sqrt(y)" y$ C. c. ?& j+ U 许多程序员刚开始会用 Python 语言写一些简单的脚本,当编写脚本时,通常习惯了直接将其写为全局变量,例如上面的代码。但是,由于全局变量和局部变量实现方式不同,定义在全局范围内的代码运行速度会比定义在函数中的慢不少。通过将脚本语句放入到函数中,通常可带来 15% - 30% 的速度提升。 # 推荐写法。代码耗时:20.6秒import math4 w" O+ ?. u7 \8 B" I: w 1 d2 a" R; o1 _; Z, X' d def main(): # 定义到函数中,以减少全部变量使用 size = 10000 for x in range(size):) C9 _% J8 v! h3 P4 {* ~6 A for y in range(size): z = math.sqrt(x) + math.sqrt(y): Y3 s V: O, z3 c! L7 m* G ) X+ D" u. |) R main()7 G1 H& `4 c; I 2. 避免.(属性访问操作符) 2.1 避免模块和函数属性访问 # 不推荐写法。代码耗时:14.5秒 import math, X: ?% c, k0 N& @& [6 V def computeSqrt(size: int): result = []9 c6 @3 D! l6 M, N for i in range(size):, T0 J0 j% n) C+ I result.append(math.sqrt(i))0 q6 I) T. Y) `7 Q: I return result / N7 e5 X2 V% B# K. k def main(): size = 10000- L8 x3 Q7 a. H9 j: _; u/ b for _ in range(size): result = computeSqrt(size): ^4 d6 D4 n( y main() 每次使用.(属性访问操作符时)会触发特定的方法,如__getattribute__()和__getattr__(),这些方法会进行字典操作,因此会带来额外的时间开销。通过from import语句,可以消除属性访问。 # 第一次优化写法。代码耗时:10.9秒from math import sqrt$ r) I1 R4 v1 d5 v! U! |1 F def computeSqrt(size: int): result = []' Z* p& f# T5 _0 G/ _% {4 ? for i in range(size): result.append(sqrt(i)) # 避免math.sqrt的使用 return result9 N @ V7 Z3 q1 I/ v def main():' ?3 ]: s# v$ C7 Y& {- M4 ~' ?7 F size = 10000 for _ in range(size): result = computeSqrt(size) main(). r7 M6 q2 E' @9 G6 F 在第 1 节中我们讲到,局部变量的查找会比全局变量更快,因此对于频繁访问的变量sqrt,通过将其改为局部变量可以加速运行。 # 第二次优化写法。代码耗时:9.9秒( H6 C* d% b, H0 S" s, ^* ?3 Aimport math def computeSqrt(size: int):- ?0 Q! x! f) r1 N& b' _ result = []3 L, b! k7 [$ W( ^7 n sqrt = math.sqrt # 赋值给局部变量 for i in range(size):( u1 w* `2 u6 b- |$ T result.append(sqrt(i)) # 避免math.sqrt的使用& Q* @% T! \( t& _: e9 T* ~* Y6 j' \ return result0 f: u1 c/ k" g) [/ L def main(): size = 100009 d ?7 E% Y6 e+ x4 d* F for _ in range(size): result = computeSqrt(size) 8 K6 G9 d2 m4 ~5 l- S main() 除了math.sqrt外,computeSqrt函数中还有.的存在,那就是调用list的append方法。通过将该方法赋值给一个局部变量,可以彻底消除computeSqrt函数中for循环内部的.使用。 # 推荐写法。代码耗时:7.9秒import math 5 S4 v* b$ ~, _( D: E7 ~8 l def computeSqrt(size: int):6 q" K& K1 |/ j5 K/ ]9 } result = []8 P% |% k+ s$ ~ U append = result.append$ ?& i" M4 u5 O* d$ A sqrt = math.sqrt # 赋值给局部变量 for i in range(size):) |- U% s0 H" {- l. k) J& s append(sqrt(i)) # 避免 result.append 和 math.sqrt 的使用6 E& a V1 n9 k* T1 y% B. }9 `1 ~ return result def main():& b/ H f+ W8 I$ F# f6 R size = 10000 for _ in range(size):+ ^( w/ D; ~3 g$ F) z6 y3 l result = computeSqrt(size)6 g5 q: h8 h$ }) \/ \ - o! Z+ ?" x+ T4 G main() 2.2 避免类内属性访问 # 不推荐写法。代码耗时:10.4秒 X% N8 }0 d0 _, b5 Q import math from typing import List- R8 w& W$ E. s9 { class DemoClass: def __init__(self, value: int):+ M7 O$ q' b7 q! O& p self._value = value def computeSqrt(self, size: int) -> List[float]: result = [] append = result.append sqrt = math.sqrt) V3 ?. R4 w4 S- D& [/ W& J for _ in range(size): append(sqrt(self._value))4 f5 @$ q+ X1 a7 L- ` return result) X- E( J. l9 u( Y* Y! B def main(): size = 10000' W, h- q. a; Z* V- z5 E1 j; k for _ in range(size):' T6 k2 Z9 y' r/ }# D6 |' j demo_instance = DemoClass(size)4 o0 a! M, F! Y: ~& F4 W result = demo_instance.computeSqrt(size)7 f) n' h* P8 e; K+ g main()6 ?. Q. G2 k. t+ n 避免.的原则也适用于类内属性,访问self._value的速度会比访问一个局部变量更慢一些。通过将需要频繁访问的类内属性赋值给一个局部变量,可以提升代码运行速度。 # 推荐写法。代码耗时:8.0秒import math from typing import List class DemoClass: def __init__(self, value: int): self._value = value ( [/ {4 R4 B+ S# T2 t' H def computeSqrt(self, size: int) -> List[float]: result = []5 u2 u5 H2 ]0 x& V% M, e! O append = result.append sqrt = math.sqrt7 D" C" n) D9 @# [6 F value = self._value7 H: k O, N! t& M" F# ] for _ in range(size):+ N5 Z4 B/ t! _( D append(sqrt(value)) # 避免 self._value 的使用 return result 4 h- X, t. z; S# C! M+ N def main(): size = 100006 o/ M% I0 ^" S# C7 _ for _ in range(size): demo_instance = DemoClass(size) demo_instance.computeSqrt(size)' F0 L8 r+ s& I2 z }: S main()& j, b1 I: T$ n 3. 避免不必要的抽象, }, l' u9 L" B* |- n. b2 F # 不推荐写法,代码耗时:0.55秒 class DemoClass: def __init__(self, value: int):1 ]) C1 d) F' u5 ? self.value = value+ \( s. d3 c3 t' q0 I3 Q @property def value(self) -> int:' w0 f' r6 }( u! C3 h( _6 T$ f return self._value @value.setter def value(self, x: int): self._value = x- \! G+ L( N% r def main():0 _5 e: o! x& ~& d$ n' y+ x size = 1000000 for i in range(size):1 }$ i1 [, }3 k demo_instance = DemoClass(size) value = demo_instance.value demo_instance.value = i 7 ^% d8 ~; R2 @% m3 O) L main() 任何时候当你使用额外的处理层(比如装饰器、属性访问、描述器)去包装代码时,都会让代码变慢。大部分情况下,需要重新进行审视使用属性访问器的定义是否有必要,使用getter/setter函数对属性进行访问通常是 C/C++ 程序员遗留下来的代码风格。如果真的没有必要,就使用简单属性。 # 推荐写法,代码耗时:0.33秒+ \3 ~. ]! U! \9 q* Zclass DemoClass: def __init__(self, value: int):6 Q4 F& ^9 }5 }$ {9 C. N) a/ w self.value = value # 避免不必要的属性访问器 $ @3 X R+ i8 H2 N' ]- V def main(): size = 10000005 C! T1 R" a' [* R7 D9 I" I4 Z% `' ~ for i in range(size):5 Z9 C5 l' y1 l" n demo_instance = DemoClass(size) value = demo_instance.value. {( b0 G3 p$ `2 t' \) ?. o* ~& q2 C demo_instance.value = i/ |' d B. E a7 z2 J! N r: r2 @ 0 T" S$ Z9 @) J( d8 Q+ ]; U main()* z) J# x3 a6 ^# k9 Y ] + a/ P) {' e3 I d: t5 j8 v; J 4. 避免数据复制$ F0 A5 Z8 Q W7 g) @* N7 S 4.1 避免无意义的数据复制 # 不推荐写法,代码耗时:6.5秒 def main():* i A2 V$ P5 h3 j/ t# D* ] size = 100000 p! f$ h8 V% _1 j7 B for _ in range(size): value = range(size) value_list = [x for x in value] square_list = [x * x for x in value_list]% y( } |/ e6 ?' B8 O main() 上面的代码中value_list完全没有必要,这会创建不必要的数据结构或复制。 # 推荐写法,代码耗时:4.8秒) x& J% v/ j8 x/ E) M8 b: Xdef main():' y K6 G. c) a! O( K' ?7 p size = 100007 p4 }* z" T* N% K for _ in range(size):# Y" L# E" ?( f value = range(size) square_list = [x * x for x in value] # 避免无意义的复制 main(). _6 i% d/ p6 U 另外一种情况是对 Python 的数据共享机制过于偏执,并没有很好地理解或信任 Python 的内存模型,滥用 copy.deepcopy()之类的函数。通常在这些代码中是可以去掉复制操作的。 4.2 交换值时不使用中间变量6 Z* P5 j2 [% q0 p- U7 K# 不推荐写法,代码耗时:0.07秒 }9 l* B D' j" N5 Y+ w' S def main():) z" g# f: b" }* P# O+ Q size = 1000000$ F8 ]6 B. g- [/ }4 m& N for _ in range(size):. R W, d! H6 c7 ^" f9 D a = 3 b = 5 temp = a a = b b = temp / K1 g% Y- L% m. Z1 z, {8 [ main()3 |! y! W& e- V9 ^& S 上面的代码在交换值时创建了一个临时变量temp,如果不借助中间变量,代码更为简洁、且运行速度更快。 # 推荐写法,代码耗时:0.06秒4 Q! A' I; F: v! x! a, }def main():) y# `! J, n7 \/ q( q size = 10000009 [( O* f, I2 d8 k% u for _ in range(size):* M" z) g) M: [1 i# J1 V( { a = 35 ?3 W" _6 P9 x; r' z b = 5 a, b = b, a # 不借助中间变量" K4 Y/ T- m: U; S5 I& c% s5 V ! b+ s8 A4 j( m. M/ A4 F$ l main()# X0 X! ?2 F+ ^& c 4.3 字符串拼接用join而不是+ # 不推荐写法,代码耗时:2.6秒 import string) {$ a* J, O0 v4 {/ |( v! M% ~$ w! z from typing import List! Q6 p+ D! E8 X$ h+ K def concatString(string_list: List[str]) -> str: result = ''' A# U. _ _' H% A for str_i in string_list: result += str_i return result0 B; E- e. i1 i5 v9 B ; A' a. E2 O9 {- ^6 s8 R$ h+ O( _+ T, h def main():* o+ b: p" `8 u: X" a1 ?( O string_list = list(string.ascii_letters * 100) for _ in range(10000): result = concatString(string_list): h3 o. H \$ {1 ~/ q. Q4 j5 s main() 当使用a + b拼接字符串时,由于 Python 中字符串是不可变对象,其会申请一块内存空间,将a和b分别复制到该新申请的内存空间中。因此,如果要拼接 个字符串,会产生 个中间结果,每产生一个中间结果都需要申请和复制一次内存,严重影响运行效率。而使用join()拼接字符串时,会首先计算出需要申请的总的内存空间,然后一次性地申请所需内存,并将每个字符串元素复制到该内存中去。 # 推荐写法,代码耗时:0.3秒) ?' b& T k$ n5 }- C. N4 g. Rimport string8 B& p( Z7 k2 C' o4 [4 ] from typing import List 3 w& i! `+ J' X3 w4 } def concatString(string_list: List[str]) -> str: return ''.join(string_list) # 使用 join 而不是 +* q o! ]/ u) v' o ) P( g% J7 @! N, | def main():& T4 t, i3 I5 s C5 d# [ V$ y string_list = list(string.ascii_letters * 100) for _ in range(10000): result = concatString(string_list): s: \: J) ?1 V# K1 G9 D main()# L+ i% v0 Q8 y, ? * ~4 U, D. i' `. d 5. 利用if条件的短路特性, R4 t% ?: V2 M( X2 W) D # 不推荐写法,代码耗时:0.05秒$ [% s" K- r, D) L1 a from typing import List ; b3 \6 t4 B2 F* N0 ?: H- s def concatString(string_list: List[str]) -> str:2 ^- l9 _% j0 U0 L, Q1 r5 c3 Y abbreviations = {'cf.', 'e.g.', 'ex.', 'etc.', 'flg.', 'i.e.', 'Mr.', 'vs.'}9 Z. g' v0 u4 ?4 I% t J# T6 i- o: Z" v abbr_count = 0 result = '' for str_i in string_list:: b. S+ g6 d( e% u if str_i in abbreviations: result += str_i return result " Y0 y) ~6 Y( f% ]' v0 I& Q* @9 P def main(): for _ in range(10000):8 m& c: F. H# A7 r string_list = ['Mr.', 'Hat', 'is', 'Chasing', 'the', 'black', 'cat', '.']: }! u' A: i* d* Y result = concatString(string_list) # F$ x- N+ {) S4 y5 | main() if 条件的短路特性是指对if a and b这样的语句, 当a为False时将直接返回,不再计算b;对于if a or b这样的语句,当a为True时将直接返回,不再计算b。因此, 为了节约运行时间,对于or语句,应该将值为True可能性比较高的变量写在or前,而and应该推后。 # 推荐写法,代码耗时:0.03秒from typing import List) ?6 N/ ?+ M4 T7 K ( @' u( ^+ q' E2 R1 g: N def concatString(string_list: List[str]) -> str:- O9 L* p) S# a5 u) Z2 }1 F abbreviations = {'cf.', 'e.g.', 'ex.', 'etc.', 'flg.', 'i.e.', 'Mr.', 'vs.'} abbr_count = 05 { p" x7 F9 d" _; t& [0 H+ n result = '' for str_i in string_list:/ N6 v) Y' f" i5 V if str_i[-1] == '.' and str_i in abbreviations: # 利用 if 条件的短路特性. Q! M* ]$ ~7 i" J: y" } result += str_i return result def main(): for _ in range(10000): string_list = ['Mr.', 'Hat', 'is', 'Chasing', 'the', 'black', 'cat', '.']2 [1 K6 G1 x2 ? result = concatString(string_list) : M* _5 v' l+ j! w main() 5 d8 v' f' B7 T1 t, z% O" ` 6. 循环优化$ B5 {; u7 L6 z6 Q% G6 Q- k 6.1 用for循环代替while循环' o3 S* s8 h' s2 c4 t t8 d # 不推荐写法。代码耗时:6.7秒, ]$ U4 x% c4 P0 u! r def computeSum(size: int) -> int:% \' k. b* ^4 I! V3 z9 M sum_ = 0+ D" k0 V" ?. ]( x) v# v w i = 0: n4 r4 ^8 d) f& A L while i < size:+ X& O h& `# s+ P8 K sum_ += i i += 1) |1 w$ D( z5 c7 [* O5 Y9 X return sum_. v/ k; p. [5 M( j def main():/ e* ?0 t/ }" j; g size = 10000 for _ in range(size):$ u6 y+ ~5 E# _' l sum_ = computeSum(size)8 ~% V0 F8 e$ g main(); m- ~; ^* P, J Python 的for循环比while循环快不少。 # 推荐写法。代码耗时:4.3秒def computeSum(size: int) -> int:5 _; Z6 T5 w o* o7 Z sum_ = 0 for i in range(size): # for 循环代替 while 循环 sum_ += i! w6 M4 O1 o0 F3 _' g7 l1 r return sum_% v+ D |2 K1 \. K) T" [% d 7 x4 h4 m% O8 b9 Y& D! [/ _. ?. o$ T* L def main(): size = 100002 z& [1 s% x- k3 g1 v- K for _ in range(size):8 A( e+ z! B5 d) ~0 q0 ? sum_ = computeSum(size) ) S- i1 g& T, f) L5 R% k; C main() 6.2 使用隐式for循环代替显式for循环 针对上面的例子,更进一步可以用隐式for循环来替代显式for循环 # 推荐写法。代码耗时:1.7秒7 N6 _2 ^& h3 Ydef computeSum(size: int) -> int:& F7 w3 r0 l! O# N( k$ ]$ C% I return sum(range(size)) # 隐式 for 循环代替显式 for 循环# R( x2 T! r: x4 M C def main():% |7 N3 {; Y- V6 y/ H5 Y; D) C size = 10000 for _ in range(size): sum = computeSum(size) main()/ ^( P: Y# U Q7 A- I$ E: L& Q& R 6.3 减少内层for循环的计算) A) `9 ^) B# q9 G1 j# d # 不推荐写法。代码耗时:12.8秒 import math def main(): size = 100003 |: i) P' ?5 F0 f+ ]" J0 g sqrt = math.sqrt for x in range(size):! ?* ^- u6 b6 U o for y in range(size): z = sqrt(x) + sqrt(y) / ~" v+ P, M% h0 S$ r: t main() 上面的代码中sqrt(x)位于内侧for循环, 每次训练过程中都会重新计算一次,增加了时间开销。 # 推荐写法。代码耗时:7.0秒import math, C8 @ K4 x) _ def main(): size = 10000 sqrt = math.sqrt for x in range(size):5 m1 h Y6 \7 ]* ^3 x7 i sqrt_x = sqrt(x) # 减少内层 for 循环的计算5 O) N5 c1 y/ r4 a5 e% \0 k for y in range(size): z = sqrt_x + sqrt(y), s* m% v R+ {8 k main()4 W {, P% d$ q8 m $ Q0 Z9 K$ d8 P5 @# k' |, d- ^! x( m 7. 使用numba.jit 我们沿用上面介绍过的例子,在此基础上使用numba.jit。numba可以将 Python 函数 JIT 编译为机器码执行,大大提高代码运行速度。关于numba的更多信息见下面的主页: http://numba.pydata.org/numba.pydata.org # 推荐写法。代码耗时:0.62秒import numba # }4 p, d, R% {8 o @numba.jit def computeSum(size: float) -> int:8 @) f- Q8 d; U! r9 _7 { sum = 0 for i in range(size): sum += i return sum def main(): size = 10000$ {( }& p. @6 J0 _. T for _ in range(size):; e$ Q/ e& w Z. \( L u) j9 E8 f sum = computeSum(size)" J! i A; P( R- L A7 |4 F 6 c! w7 Y9 h" h0 K# z5 \6 v$ O+ ~ main() - r' B: k2 ~4 I; l& H& S 8. 选择合适的数据结构 Python 内置的数据结构如str, tuple, list, set, dict底层都是 C 实现的,速度非常快,自己实现新的数据结构想在性能上达到内置的速度几乎是不可能的。 list类似于 C++ 中的std::vector,是一种动态数组。其会预分配一定内存空间,当预分配的内存空间用完,又继续向其中添加元素时,会申请一块更大的内存空间,然后将原有的所有元素都复制过去,之后销毁之前的内存空间,再插入新元素。删除元素时操作类似,当已使用内存空间比预分配内存空间的一半还少时,会另外申请一块小内存,做一次元素复制,之后销毁原有大内存空间。因此,如果有频繁的新增、删除操作,新增、删除的元素数量又很多时,list的效率不高。此时,应该考虑使用collections.deque。collections.deque是双端队列,同时具备栈和队列的特性,能够在两端进行 复杂度的插入和删除操作。 list的查找操作也非常耗时。当需要在list频繁查找某些元素,或频繁有序访问这些元素时,可以使用bisect维护list对象有序并在其中进行二分查找,提升查找的效率。 另外一个常见需求是查找极小值或极大值,此时可以使用heapq模块将list转化为一个堆,使得获取最小值的时间复杂度是 。 下面的网页给出了常用的 Python 数据结构的各项操作的时间复杂度: TimeComplexity - Python Wikiwiki.python.org 4 G% k# s2 w l/ L' F$ n3 M |