一、代码优化原则
(整理自 https://mp.weixin.qq.com/s/zBjfGihZYY6XcVdGkyQfww)
介绍具体优化规则之前,总结三条基本原则。
1. 不要过早优化
代码优化的前提是代码已经能正常工作。始终牢记 让正确的程序更快比让快速的程序正确容易得多。
2.权衡优化的代价
不存在完美的程序,通常的优化策略都是时间换空间,空间换时间。
3.不优化无关紧要的部分
如果对代码的每个部分进行优化,最后的结果是你会写出一个及其晦涩难懂的程序。因此,只优化那些耗时较久的部分,通常是内部循环。
二、代码优化建议
1. 避免全局变量
# 不推荐写法 运行时长:27.04s
import math
size = 10000
for x in range(size):
for y in range(size):
z = math.sqrt(x) + math.sqrt(y)
这段代码中定义了一个全局变量size
,我们试着把这段代码封装到一个函数里,看下运行时间。
# 推荐写法 运行时间:22.01s
import math
def main():
size = 10000
for x in range(size):
for y in range(size):
z = math.sqrt(x) + math.sqrt(y)
main()
程序运行时间缩短了!
2.避免.
2.1 避免模块和函数属性访问
例如这个例子中的math.sqrt()
和result.append()
.每次访问.
会触发特定的方法,如__getattribute__()
和__getattr__()
,这些方法会进行字典的相关操作,因此会带来额外的时间开销。
# 运行时间:15.92s
import math
def computeSqrt(size:int):
result = []
for i in range(size):
result.append(math.sqrt(i))
return result
def main():
size = 10000
for _ in range(size):
result = computeSqrt(size)
main()
可以看到,模块和函数属性的访问写在了for
循环里,因此,程序运行的效率可以提高。首先,我们使用from import
语句消除模块属性访问。
# 运行时间:13.96s
from math import sqrt
def computeSqrt(size: int):
result = []
for i in range(size):
result.append(sqrt(i)) # 避免math.sqrt的使用
return result
def main():
size = 10000
for _ in range(size):
result = computeSqrt(size)
main()
可以看到,运行时间缩短了,更进一步,我们根据第一条规则把sqrt
从全局变量转为局部变量。
# 运行时间:12.62s
import math
def computeSqrt(size: int):
result = []
sqrt = math.sqrt # 赋值给局部变量
for i in range(size):
result.append(sqrt(i)) # 避免math.sqrt的使用
return result
def main():
size = 10000
for _ in range(size):
result = computeSqrt(size)
main()
代码运行时间进一步缩短。但此时for
循环里还有.
的存在,即result.append()
,我们再优化一下这部分。
# 运行时间:8.10s
import math
def computeSqrt(size: int):
result = []
append = result.append
sqrt = math.sqrt # 赋值给局部变量
for i in range(size):
append(sqrt(i)) # 避免 result.append 和 math.sqrt 的使用
return result
def main():
size = 10000
for _ in range(size):
result = computeSqrt(size)
main()
运行时间大大缩短。因此从程序运行时间上来讲,这种方式是最优的,然而需要了解的是这种写法牺牲了程序的部分简洁性和可读性。因此使用的时候可以考虑这种极致的优化是否必要。
2.2 避免类内属性访问
避免.
的原则同样适用于类内属性,访问self._value
的速度会比访问一个局部变量更慢一些。如果需要频繁的访问类内属性,可以先将其赋值给局部变量。例如:
# 频繁访问了self._value。代码耗时:11.77s
import math
from typing import List
class DemoClass:
def __init__(self, value: int):
self._value = value
def computeSqrt(self, size: int) -> List[float]:
result = []
append = result.append
sqrt = math.sqrt
for _ in range(size):
append(sqrt(self._value))
return result
def main():
size = 10000
for _ in range(size):
demo_instance = DemoClass(size)
result = demo_instance.computeSqrt(size)
main()
优化后
# 推荐写法。代码耗时:8.07s
import math
from typing import List
class DemoClass:
def __init__(self, value: int):
self._value = value
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
def main():
size = 10000
for _ in range(size):
demo_instance = DemoClass(size)
demo_instance.computeSqrt(size)
main()
代码运行时间大大缩短。
3.拼接字符串用join
而不是+
python中字符串是不可变对象,因此在使用a+b
时,会另外申请一块内存空间,将a
和b
分别复制到新申请的内存空间中。因此如果需要拼接n个字符串,会有n-1个中间结果产生,每产生一个中间结果都会申请和复制一次内存,将会大大影响运行效率。而使用join()
拼接字符串时,会首先计算出需要申请的总内存空间,然后一次性申请到位,并将需要拼接的每个字符串复制到该内存中。
4.利用if
的短路特性
if
的短路特性指的是:
- 对于
if a and b
,当a
为False
时,直接返回False
,不再计算b
。 - 对于
if a or b
,当a
为True
时,直接返回True
,不再计算b
。
因此,利用该特性,对于or
语句,应该将值为True
概率更高的语句放前面,而对于and
语句,应该将值为False
概率更高的语句放前面。
5.循环优化
-
for
循环优于while
- 隐式循环代替显示循环。例
sum(range(a))
.
6.选择合适的数据结构
python内置的数据结构如str
,tuple
,list
,set
,dict
都是c实现的,速度很快。我们自己新实现的数据结构想在性能上达到内置的速度几乎是不可能的。因此了解这些数据结构的特性并使用好非常重要。
举例:list
是一个动态数组,在初始化时会为其预分配内存空间。当需要向其添加元素时,需要申请一个更大的空间,然后将list原有的元素都复制过去,之后销毁原空间,最后插入新的元素。如果又频繁的新增、删除操作且新增、删除的元素数量很多时,list的效率不高。此时可以使用其他数据结构,如collections.deque
双端队列,同时具备栈和队列的特性,能够在两端进行O(1)
复杂度的插入和删除操作。
7.交换元素值
a,b = b,a #无需创建中间变量