|
Python 是一种脚本语言,相比 C/C++ 这样的编译语言,在效率和性能方面存在一些不足。但是,有很多时候,Python 的效率并没有想象中的那么夸张。本文对一些 Python 代码加速运行的技巧进行整理。
0. 代码优化原则 本文会介绍不少的 Python 代码加速运行的技巧。在深入代码优化细节之前,需要了解一些代码优化基本原则。 第一个基本原则是不要过早优化。很多人一开始写代码就奔着性能优化的目标,“让正确的程序更快要比让快速的程序正确容易得多”。因此,优化的前提是代码能正常工作。过早地进行优化可能会忽视对总体性能指标的把握,在得到全局结果前不要主次颠倒。 第二个基本原则是权衡优化的代价。优化是有代价的,想解决所有性能的问题是几乎不可能的。通常面临的选择是时间换空间或空间换时间。另外,开发代价也需要考虑。 第三个原则是不要优化那些无关紧要的部分。如果对代码的每一部分都去优化,这些修改会使代码难以阅读和理解。如果你的代码运行速度很慢,首先要找到代码运行慢的位置,通常是内部循环,专注于运行慢的地方进行优化。在其他地方,一点时间上的损失没有什么影响。 . G* o- v+ G4 g% \ 1. 避免全局变量# 不推荐写法。代码耗时:26.8秒+ [$ K7 U, ?$ l6 ~ import math2 w: Q' O6 W* [% t5 H size = 100002 U( l/ C7 H# Y5 S! ^" g) y for x in range(size):, x! q$ s# q% m$ Q# B for y in range(size):( m$ n+ O# V2 q7 J z = math.sqrt(x) + math.sqrt(y) 许多程序员刚开始会用 Python 语言写一些简单的脚本,当编写脚本时,通常习惯了直接将其写为全局变量,例如上面的代码。但是,由于全局变量和局部变量实现方式不同,定义在全局范围内的代码运行速度会比定义在函数中的慢不少。通过将脚本语句放入到函数中,通常可带来 15% - 30% 的速度提升。 # 推荐写法。代码耗时:20.6秒import math def main(): # 定义到函数中,以减少全部变量使用 size = 100002 ^: ^' Z/ m9 a n* J5 e6 W for x in range(size): for y in range(size): z = math.sqrt(x) + math.sqrt(y)* e; P+ v# k2 @4 { . y) e$ n6 d* Q( N main() 2. 避免.(属性访问操作符) 2.1 避免模块和函数属性访问 # 不推荐写法。代码耗时:14.5秒7 y4 k W5 d) o. z import math def computeSqrt(size: int):/ V0 _6 ~) d6 C' V5 O5 Y9 b result = []* ?! N5 D, x) d% k0 n& p for i in range(size):" z3 s2 P& t* a* }2 E result.append(math.sqrt(i))1 U& v% R& z2 E' \# i% o return result( }, `9 R, ]7 f* i) F( ^) S! h def main():, _: O8 C/ d9 h size = 10000! ~* F; @7 G0 X; l. R7 X$ O/ i: W for _ in range(size): result = computeSqrt(size)! P. @ z" f; X8 ^ O0 s' [ . T6 q* B. S+ ~4 w. b main() 每次使用.(属性访问操作符时)会触发特定的方法,如__getattribute__()和__getattr__(),这些方法会进行字典操作,因此会带来额外的时间开销。通过from import语句,可以消除属性访问。 # 第一次优化写法。代码耗时:10.9秒, x. n" @! i% c) D; g! |( c Ufrom math import sqrt7 M8 f( u. Q2 w7 o: i 7 [# x7 f4 v# a3 l3 h: A6 t: S def computeSqrt(size: int):, ~% y( T& ~7 }' C result = [] for i in range(size): result.append(sqrt(i)) # 避免math.sqrt的使用 return result) n+ f6 c' Z4 W % k N8 c- S3 T$ |$ {' V3 V3 i def main(): size = 10000 for _ in range(size): result = computeSqrt(size)6 j6 _3 R4 e# r/ z) W' }% g main() 在第 1 节中我们讲到,局部变量的查找会比全局变量更快,因此对于频繁访问的变量sqrt,通过将其改为局部变量可以加速运行。 # 第二次优化写法。代码耗时:9.9秒import math def computeSqrt(size: int): result = []# i. v! g7 j" p& |0 \ sqrt = math.sqrt # 赋值给局部变量1 G. s5 f. b3 V& R for i in range(size):6 v# C! ?4 S# O9 @8 O* I& o result.append(sqrt(i)) # 避免math.sqrt的使用 return result. O K, G1 D% E, L3 }' { ! v8 c" a, X+ \$ l def main():4 O5 g2 T" j; b& T* E size = 10000) c N- ~8 s1 ^. { for _ in range(size): result = computeSqrt(size)( I- T3 b9 ]% K p+ a q$ ` main()& c Q( n+ u8 m9 H8 |+ E- O 除了math.sqrt外,computeSqrt函数中还有.的存在,那就是调用list的append方法。通过将该方法赋值给一个局部变量,可以彻底消除computeSqrt函数中for循环内部的.使用。 # 推荐写法。代码耗时:7.9秒; j6 A5 _* v' @4 @0 Rimport math 1 N' u o1 k! u, \) t def computeSqrt(size: int):( E: M" V5 p. A: T result = [] append = result.append! {- T2 S% z8 q4 p2 }1 E* a sqrt = math.sqrt # 赋值给局部变量 for i in range(size):7 Q. Y0 U, v r5 A5 K$ E append(sqrt(i)) # 避免 result.append 和 math.sqrt 的使用# x% g: E% E* X4 X return result def main(): size = 10000 for _ in range(size):2 @. h0 r7 T& z1 }: T7 C2 J5 o result = computeSqrt(size) - c' {8 w* r7 G main() 2.2 避免类内属性访问 # 不推荐写法。代码耗时:10.4秒, F/ @8 K, M; W% p; y import math0 E% O+ M- B, ?# ^6 C8 r from typing import List1 \. O% i5 v/ L9 D" u " N D, x2 ~ ] `- F) V5 W class DemoClass: def __init__(self, value: int): self._value = value! G1 g! E% c' ?& X8 D0 U def computeSqrt(self, size: int) -> List[float]:# g/ W0 W" W2 M. a$ [( { result = [] append = result.append sqrt = math.sqrt7 K! Z/ H* h$ t2 G for _ in range(size): g: n* O* @3 b/ ]1 \ append(sqrt(self._value)) return result. Z1 e; l0 U! \2 U5 `! c; q( N. P def main():: M! u% ?" b" v; u3 k size = 10000& e3 I3 Z% I# O1 s* X0 C7 D- C# G" ~ for _ in range(size): demo_instance = DemoClass(size) result = demo_instance.computeSqrt(size)5 ]1 z6 I# |1 `! X" m7 h7 M main()) |" I' j/ p, L, R 避免.的原则也适用于类内属性,访问self._value的速度会比访问一个局部变量更慢一些。通过将需要频繁访问的类内属性赋值给一个局部变量,可以提升代码运行速度。 # 推荐写法。代码耗时:8.0秒import math- Y2 t4 L: P4 R- r7 e from typing import List* J7 b8 Z+ k6 b' V) m* l2 i 8 N2 @$ g( U, A8 I9 v; r( v J class DemoClass: def __init__(self, value: int):1 o, b+ }. J3 Y# E4 }9 b' _) Z/ G self._value = value' J% d4 L9 m" e9 d def computeSqrt(self, size: int) -> List[float]: result = [] append = result.append sqrt = math.sqrt value = self._value for _ in range(size): append(sqrt(value)) # 避免 self._value 的使用 return result% d! _9 U" B( U" M7 C$ w 1 S; K( l. v2 b* u; `* y def main(): size = 10000 for _ in range(size):; B( }1 V' C% c" W demo_instance = DemoClass(size) demo_instance.computeSqrt(size)" o; h T& I. Q! s2 [ main() 3. 避免不必要的抽象1 B, S5 P& @* q # 不推荐写法,代码耗时:0.55秒: L1 T* Y( g2 k# K1 L( z class DemoClass:. G3 Q$ V! k5 F( @" R; {; j4 |0 H def __init__(self, value: int):6 I$ s, l* r ?8 f& H2 ~* } self.value = value3 l; c; w7 c- G: U& v + X q- D3 x. `- Z @property def value(self) -> int:& |2 I! F' V+ p0 W* H6 \5 A return self._value @value.setter, \1 p; c: `/ _: { def value(self, x: int):; W0 I3 K& v) t2 P self._value = x def main():. G$ w& ]& W! v: S, g size = 1000000 for i in range(size):2 C1 `. h* B: g" I! t6 i0 \, ~ demo_instance = DemoClass(size)0 ^ p# k3 X9 s w value = demo_instance.value4 U& }4 T1 _) j7 I# m; v* e( u* ? demo_instance.value = i& g" u( |/ S, c9 X main() 任何时候当你使用额外的处理层(比如装饰器、属性访问、描述器)去包装代码时,都会让代码变慢。大部分情况下,需要重新进行审视使用属性访问器的定义是否有必要,使用getter/setter函数对属性进行访问通常是 C/C++ 程序员遗留下来的代码风格。如果真的没有必要,就使用简单属性。 # 推荐写法,代码耗时:0.33秒class DemoClass:2 ~ X, k. K" n5 G8 U% v6 J def __init__(self, value: int): self.value = value # 避免不必要的属性访问器- F$ c3 U# x$ d0 w w( @ 2 h* }! |* l, F8 e) v8 J def main(): M, F( v3 |4 z" C D9 ^ size = 1000000 for i in range(size):: i" v3 {$ Q+ X7 v# D demo_instance = DemoClass(size)7 V0 |$ e, r; D+ [& i C! K value = demo_instance.value demo_instance.value = i , m* F( h5 ^" N* B J; b: D: v main() 0 c' R& M7 z- \5 P+ Y, y 4. 避免数据复制% D1 B- z1 {& i. Y% n( k* z4 b 4.1 避免无意义的数据复制: n: P1 Z7 q' J, N! N # 不推荐写法,代码耗时:6.5秒 def main(): size = 10000 for _ in range(size):# ]' |. ]* b" u+ B* R4 j. G' S value = range(size)2 T, V- G+ `9 \0 m6 c value_list = [x for x in value]: `7 s, r" }" R; [ square_list = [x * x for x in value_list]( L" |4 ]" r! o. q, D* r8 A/ L + k! K& K8 d4 d9 H# G; ^/ K! d, C' P main() 上面的代码中value_list完全没有必要,这会创建不必要的数据结构或复制。 # 推荐写法,代码耗时:4.8秒def main(): size = 10000 A% d7 q$ D" | for _ in range(size):( P( q0 f3 b$ Q/ L value = range(size)$ r& {3 V V4 v3 U4 }5 c square_list = [x * x for x in value] # 避免无意义的复制 main(). C K! O6 L2 U1 U s7 e" ^, z. e 另外一种情况是对 Python 的数据共享机制过于偏执,并没有很好地理解或信任 Python 的内存模型,滥用 copy.deepcopy()之类的函数。通常在这些代码中是可以去掉复制操作的。 4.2 交换值时不使用中间变量# 不推荐写法,代码耗时:0.07秒. d: x4 w( k9 H3 W! Z. T2 w def main(): size = 1000000 for _ in range(size):- \. l# P5 j6 g+ n0 d2 y( t5 B a = 3/ S; P) B& s1 _! L9 o& A b = 5 temp = a# W8 p. o" m+ L a = b$ c# K8 N3 ^2 T8 F3 M b = temp7 [1 B) q8 F( n1 Q$ I: v* G | main() 上面的代码在交换值时创建了一个临时变量temp,如果不借助中间变量,代码更为简洁、且运行速度更快。 # 推荐写法,代码耗时:0.06秒def main():. R3 c% A. ?. m size = 10000001 f8 r& |, V7 s" O$ m- `! U for _ in range(size):4 R3 x3 R4 k' B, Q a = 3 b = 5: R6 m/ L* M. z5 L a, b = b, a # 不借助中间变量. {* L) m" L2 w5 u$ P 8 O- d2 ?! H8 T" k4 a main(); v# C( v, H3 i8 C 4.3 字符串拼接用join而不是+ # 不推荐写法,代码耗时:2.6秒+ u3 G8 A0 t. Q6 ? import string from typing import List# M: \ t# ] Z2 X! L0 }5 m def concatString(string_list: List[str]) -> str:* U6 S1 T& ~3 E* S K result = '' for str_i in string_list: result += str_i I. U. _- ?# f- S \ return result. ~, @: `8 r: b( g def main(): string_list = list(string.ascii_letters * 100) for _ in range(10000):4 s9 {5 O! l6 ]1 G) x8 N0 v result = concatString(string_list) 4 E ~$ l$ W; B- J: W main() 当使用a + b拼接字符串时,由于 Python 中字符串是不可变对象,其会申请一块内存空间,将a和b分别复制到该新申请的内存空间中。因此,如果要拼接 个字符串,会产生 个中间结果,每产生一个中间结果都需要申请和复制一次内存,严重影响运行效率。而使用join()拼接字符串时,会首先计算出需要申请的总的内存空间,然后一次性地申请所需内存,并将每个字符串元素复制到该内存中去。 # 推荐写法,代码耗时:0.3秒( Z3 s7 N* `/ L. h, w) l3 q( Himport string q- ` f$ n; Q) C from typing import List. \, y; ]/ S4 b" |5 m8 \ 2 f4 V7 g0 u( f def concatString(string_list: List[str]) -> str: return ''.join(string_list) # 使用 join 而不是 +6 J8 ?3 o0 w1 u9 ]8 }0 H$ \ : r1 b! ?* t" m8 B2 V def main(): string_list = list(string.ascii_letters * 100) for _ in range(10000): result = concatString(string_list) main()0 }1 X) S* T$ } 5. 利用if条件的短路特性7 m! H. V5 a5 g7 r$ | # 不推荐写法,代码耗时:0.05秒* ` z4 w4 @; [6 `3 e% f, ~ from typing import List , P) @+ Q, c+ o; `4 N* H def concatString(string_list: List[str]) -> str:" C8 y' Q) p- @. q" v6 V5 } abbreviations = {'cf.', 'e.g.', 'ex.', 'etc.', 'flg.', 'i.e.', 'Mr.', 'vs.'} abbr_count = 0" a; d$ S. @- ^8 ^ result = ''; [9 i/ j2 K, K8 [ for str_i in string_list:; X& y1 [$ @& r. U2 _) j. F% I if str_i in abbreviations: result += str_i return result% N; J7 c1 M/ A5 c- p9 p9 S* e def main():; q) v" l& S" W/ C* d- J Y) c. i for _ in range(10000): string_list = ['Mr.', 'Hat', 'is', 'Chasing', 'the', 'black', 'cat', '.'] result = concatString(string_list) 9 E1 H) s* E9 }, j$ e( K* M main()5 i# D" _7 l! s: j; F2 {4 i if 条件的短路特性是指对if a and b这样的语句, 当a为False时将直接返回,不再计算b;对于if a or b这样的语句,当a为True时将直接返回,不再计算b。因此, 为了节约运行时间,对于or语句,应该将值为True可能性比较高的变量写在or前,而and应该推后。 # 推荐写法,代码耗时:0.03秒- H- r5 N9 w0 M4 N% G/ Rfrom typing import List- z6 l0 ?1 F4 S* j4 ` def concatString(string_list: List[str]) -> str:7 G' _8 o4 l1 T abbreviations = {'cf.', 'e.g.', 'ex.', 'etc.', 'flg.', 'i.e.', 'Mr.', 'vs.'}! `3 V4 [( i( h8 `" _3 u$ p! Y abbr_count = 0 result = ''8 l3 s% r% Z S4 Y o for str_i in string_list: if str_i[-1] == '.' and str_i in abbreviations: # 利用 if 条件的短路特性2 W e% e1 H: b1 a result += str_i return result+ K: e, x* H( c3 K, l- a def main():3 i# _8 G3 X; T" R for _ in range(10000): string_list = ['Mr.', 'Hat', 'is', 'Chasing', 'the', 'black', 'cat', '.']% M) E- m% ^ K& f* I result = concatString(string_list) " P+ `6 {9 ?+ B& y main() # n# o2 r; m! _1 u 6. 循环优化 6.1 用for循环代替while循环' B( w: y( U- u) L # 不推荐写法。代码耗时:6.7秒 def computeSum(size: int) -> int:1 R8 v8 ?1 B# l; H2 Y" F3 { sum_ = 0 i = 0! y( x+ ~% ]5 _# N/ a- S6 ] while i < size:) T& j6 q9 M s3 ~" l" J' B- v sum_ += i i += 1 return sum_% w' u( M. N7 p( C9 Y1 j def main(): size = 100000 y/ x+ T& _# Q r for _ in range(size):; B7 @" r& t1 I, G- q& d sum_ = computeSum(size)* {* s6 ^7 m# D( s/ H9 B. a) j 9 m6 C0 w% f" J: N r Z main() Python 的for循环比while循环快不少。 # 推荐写法。代码耗时:4.3秒' G& y1 C6 M! Q' tdef computeSum(size: int) -> int:' i! ^$ a2 s4 m- |( b sum_ = 0& s9 j% b9 ]4 [ for i in range(size): # for 循环代替 while 循环1 t9 \* r7 N3 c% Z$ @) B sum_ += i6 j4 |" H7 G/ v' Z! ^( T, m5 L5 H return sum_ def main():5 R, w6 K3 q# ]; c; G size = 100005 A" A! C6 I6 G0 V( [* U7 L for _ in range(size):+ B' T: p' ]& d& O' I sum_ = computeSum(size)1 N8 t- E; h- R) ]5 a8 p main() 6.2 使用隐式for循环代替显式for循环 针对上面的例子,更进一步可以用隐式for循环来替代显式for循环 # 推荐写法。代码耗时:1.7秒def computeSum(size: int) -> int: return sum(range(size)) # 隐式 for 循环代替显式 for 循环 4 o j0 d& o9 m" X8 [6 P: M def main(): size = 10000 for _ in range(size):8 e1 @9 L! a5 t sum = computeSum(size), L+ w3 Y0 I, N1 J+ h / l1 O! {3 k" X) q7 q" @ main()$ n. {' t: ~! n" Z* X 6.3 减少内层for循环的计算 # 不推荐写法。代码耗时:12.8秒 import math2 L9 V: K! H( z def main():: W( C1 x: ~: @% r5 C$ t1 p! b size = 10000 sqrt = math.sqrt' f `5 L9 l7 R for x in range(size): for y in range(size):, S; P: O+ K# @ z = sqrt(x) + sqrt(y) main() 上面的代码中sqrt(x)位于内侧for循环, 每次训练过程中都会重新计算一次,增加了时间开销。 # 推荐写法。代码耗时:7.0秒import math7 M/ V( P( T9 I8 P, `+ i! k' n2 J def main():6 j" B" L+ c% r0 ^9 t% O$ y, v/ r size = 10000 sqrt = math.sqrt for x in range(size): sqrt_x = sqrt(x) # 减少内层 for 循环的计算 for y in range(size):" f; n( ]7 A @9 h& F, a4 I D4 X z = sqrt_x + sqrt(y)' f k0 K6 S7 u 4 C! N# T( I' D- a# q main() 7. 使用numba.jit 我们沿用上面介绍过的例子,在此基础上使用numba.jit。numba可以将 Python 函数 JIT 编译为机器码执行,大大提高代码运行速度。关于numba的更多信息见下面的主页: http://numba.pydata.org/numba.pydata.org # 推荐写法。代码耗时:0.62秒) w' P: v! W% P4 c' n5 {4 Iimport numba 5 P; K, U3 Q7 Y9 i$ t5 e @numba.jit8 O7 F* M: Y+ _0 K' U def computeSum(size: float) -> int: sum = 0 ~& j8 M* G7 O for i in range(size): sum += i return sum i0 U- ~, N Z/ l! W0 K def main():% ^! r7 o$ j+ B7 A& ` size = 10000 for _ in range(size):- a. T, s% ?0 f1 b1 k! a9 @ sum = computeSum(size) main() 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 |
微信公众号
手机版