Python代码优化
尽量使用内置函数
改进算法,选择合适的数据结构
字典和列表
- Python 字典中使用了 hash table,因此查找操作的复杂度为 O(1),而 list 实际是个数组,在 list 中,查找需要遍历整个 list,其复杂度为 O(n),因此对成员的查找访问等操作字典要比 list 更快。
- 对于列表,在CPython中,列表被实现为长度可变的数组。数组元素为对应对象的指针,增删改查的时间复杂度和C语言的类似。不好记得话可以联想C数组的相关操作过程。为了避免每次有新元素加入list时都要调用realloc进行内存分配,python会按一定的机制提前申请多的内存空间预留下来。
操作 | 时间复杂度 |
---|---|
在尾部添加元素 | O(1) |
在指定位置插入元素 | O(N) |
获取元素 | O(1) |
修改元素 | O(1) |
删除元素 | O(N) |
遍历 | O(N) |
测试是否在列表中 | O(N) |
min()/max() | O(N) |
获取列表长度 | O(1) |
-
对于字典,CPython使用伪随机探测(pseudo-random probing)的散列表(hash table)作为字典的底层数据结构。由于这个实现细节,只有可哈希的对象才能作为字典的键。Python中所有不可变的内置类型都是可哈希的。可变类型(如列表,字典和集合)就是不可哈希的,因此不能作为字典的键。
-
字典的三个基本操作(添加元素,获取元素和删除元素)的平均事件复杂度为O(1)
-
字典保存元素的顺序不是按照放入的顺序,如果要保持放入的顺序可以考虑collections模块提供了名为OrderedDicr的有序字典,collections.defaultdict不会引发‘KeyError’异常
-
在遍历字典元素时,有一点需要特别注意。字典里的keys(), values()和items()3个方法的返回值不再是列表,而是视图对象(view objects)。
-
keys(): 返回dict_keys对象,可以查看字典所有键
-
values():返回dict_values对象,可以查看字典的所有值
-
items():返回dict_items对象,可以查看字典所有的{key, value}二元元组。
-
这样的话就意味着,字典在遍历的时候,前面的操作可以影响后面的操作
d = {1:2, 4:2} print(d) for k, v in d.items(): if k == 1: d[4] = 3 print(d) # 结果 {1: 2, 4: 2} {1: 2, 4: 3} {1: 2, 4: 3}
-
集合和列表
- set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的问题可以转换为 set 来操作。
- CPython中集合和字典非常相似。事实上,集合被实现为带有空值的字典,只有键才是实际的集合元素。此外,集合还利用这种没有值的映射做了其它的优化。由于这一点,可以快速的向集合中添加元素、删除元素、检查元素是否存在。平均时间复杂度为O(1)
循环优化
对循环的优化所遵循的原则是尽量减少循环过程中的计算量,有多重循环的尽量将内层的计算提到上一层。 下面通过实例来对比循环优化后所带来的性能的提高。
from time import time
lista = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
listb = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.01]
t = time()
for i in range(1000000):
for a in range(len(lista)):
for b in range(len(listb)):
x = lista[a] + listb[b]
print("total run time:", time() - t)
t = time()
len1 = len(lista)
len2 = len(listb)
for i in range(1000000):
for a in range(len1):
temp = lista[a]
for b in range(len2):
x = temp + listb[b]
print("total run time:", time() - t)
# total run time: 35.94349479675293
# total run time: 29.186986684799194
充分利用 Lazy if-evaluation 的特性
- if A and B:当A为False时就不会判断B了
- if A or B: 当A为True时就不会判断B了
from time import time
abbreviations = ['cf.', 'e.g.', 'ex.', 'etc.', 'fig.', 'i.e.', 'Mr.', 'vs.']
t = time()
for i in range(1000000):
for w in ('Mr.', 'Hat', 'is', 'chasing', 'the', 'black', 'cat', '.'):
if w in abbreviations:
pass
print("total run time:", time() - t)
t = time()
for i in range(1000000):
for w in ('Mr.', 'Hat', 'is', 'chasing', 'the', 'black', 'cat', '.'):
if w[-1] == '.' and w in abbreviations:
pass
print("total run time:", time() - t)
# total run time: 2.915415048599243
# total run time: 2.210136651992798
字符串优化
python 中的字符串对象是不可改变的,因此对任何字符串的操作如拼接,修改等都将产生一个新的字符串对象,而不是基于原字符串,因此这种持续的 copy 会在一定程度上影响 python 的性能。因此,如果要拼接 n 个字符串,会产生 n-1 个中间结果,每产生一个中间结果都需要申请和复制一次内存,严重影响运行效率。而使用 join() 拼接字符串时,会首先计算出需要申请的总的内存空间,然后一次性地申请所需内存,并将每个字符串元素复制到该内存中去。
- 尽量使用join 而不是 + 连接字符串
- 尽量使用内置函数来处理字符串
- 对字符进行格式化( “C%s%s”)%(“A”, “B”))比直接串联(“C”+“A”+“B”)读取要快
使用列表解析(list comprehension)和生成器表达式(generator expression)
列表解析要比在循环中重新构建一个新的 list 更为高效, 差好多
from time import time
t = time()
list = ['a','b','is','python','jason','hello','hill','with','phone','test',
'dfdf','apple','pddf','ind','basic','none','baecr','var','bana','dd','wrd']
total=[]
for i in range (1000000):
for w in list:
total.append(w)
print("total run time:", time()-t)
t = time()
for i in range (1000000):
total = [w for w in list]
print("total run time:", time()-t)
# total run time: 2.5990912914276123
# total run time: 0.8667190074920654
避免 .
避免模块和函数属性访问
每次使用.(属性访问操作符时)会触发特定的方法,如__getattribute__() 和__getattr__(),这些方法会进行字典操作,因此会带来额外的时间开销。通过 from import 语句,可以消除属性访问。
# 不推荐写法
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()
--------------------------------------------------------
# 推荐写法,第一次优化 . 模块属性访问
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,局部变量访问比全局变量快。
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()
--------------------------------------------------------
# 推荐写法, 第三次优化函数属性 list.append() 方法
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()
避免访问类内属性
避免 . 的原则也适用于类内属性,访问 self._value 的速度会比访问一个局部变量更慢一些。通过将需要频繁访问的类内属性赋值给一个局部变量,可以提升代码运行速度。
# 将self._value 赋值给局部变量
import math
from typing import List
class DemoClass:
def __init__(self, value: int):
self._value = value
# 推荐写法
def computeSqrt1(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 computeSqrt1(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)
demo_instance.computeSqrt1(size)
demo_instance.computeSqrt2(size)
main()
其他优化技巧
- 如果需要交换两个变量的值使用 a,b=b,a (更快)而不是借助中间变量 t=a;a=b;b=t;
- 使用局部变量,避免"global" 关键字。
- if done is not None 比语句 if done != None 更快。
- 使用profile模块定位瓶颈代码位置