Python性能优化全攻略:10个实用技巧大公开
前言
Python,作为一种动态类型的解释性语言,确实在执行速度上可能不如C这样的静态类型的编译语言。但是,通过一些技巧和策略,我们可以显著提升Python代码的性能。
本文将探讨如何通过优化方法使Python代码运行得更快、更高效。我们将利用Python的timeit
模块来精确测量代码的执行时间。
注意:timeit
模块在默认情况下会重复执行代码一百万次,以确保测量结果的准确性和稳定性
def print_hi(name):
print(f'Hi, {name}')
if __name__ == '__main__':
# 执行print_hi('PyCharm')方法
t = timeit.Timer(setup='from __main__ import print_hi', stmt='print_hi("PyCharm")')
t.timeit()
如何计算python脚本的运行时间呢?在time模块中time.perf_counter()
提供了一个高精度的计时器,适合测量短时间,例如
import time
# 记录程序开始时间
start_time = time.perf_counter()
# 你的代码逻辑
# ...
# 记录程序结束时间
end_time = time.perf_counter()
# 计算程序运行时间
run_time = end_time - start_time
print(f"程序运行时间:{run_time} 秒")
介绍
一、I/O密集型操作
I/O密集型操作(Input/Output Intensive Operation)指的是那些在执行过程中,大部分时间都花在等待输入/输出操作完成的程序或任务。I/O操作包括从磁盘读取数据、写入数据到磁盘、网络通信等。这些操作通常涉及到硬件设备,因此它们的执行速度受到硬件性能和I/O带宽的限制。
他们的特点有:
- 1. 等待时间:程序在执行I/O操作时,往往需要等待数据从外部设备传输到内存,或从内存传输到外部设备,这会导致程序的执行被阻塞。
- 2. CPU利用效率:由于I/O操作的等待时间,CPU在这段时间内可能处于空闲状态,导致CPU利用率不高。
- 3. 性能瓶颈:I/O操作的速度往往成为程序性能的瓶颈,尤其是在数据量大或传输速度慢的情况下。
例如,使用I/O密集型操作print,运行一百万次
import time
import timeit
def print_hi(name):
print(f'Hi, {name}')
return
if __name__ == '__main__':
start_time = time.perf_counter()
# 执行print_hi('PyCharm')方法
t = timeit.Timer(setup='from __main__ import print_hi', stmt='print_hi("PyCharm")')
t.timeit()
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"程序运行时间:{run_time} 秒")
运行结果为3s
而不使用i/o操作
执行一个方法,即调用这个print_hi('xxxx')空方法,不使用print(),程序明显快了不少
def print_hi(name):
# print(f'Hi, {name}')
return
如果代码中必要的时候,例如文件读写,可以使用如下方法提高效率
- 1. 异步I/O:使用异步编程模式例如asyncio,允许程序在等待I/O操作完成时继续执行其他任务,从而提高CPU利用率。
- 2. 缓冲:使用缓冲区暂存数据,减少I/O操作的频率。
- 3. 并行处理:并行执行多个I/O操作,以提高整体的数据处理速度。
- 4. 优化数据结构:选择合适的数据结构,减少数据的读取和写入次数。
二、使用生成器生成列表、字典
在Python 2.7及其后续版本中,引入了对列表、字典和集合生成器的改进,这些改进让数据结构的构建过程更加简明和高效。
1、传统方法
def fun1():
list=[]
for i in range(100):
list.append(i)
if __name__ == '__main__':
start_time = time.perf_counter()
t = timeit.Timer(setup='from __main__ import fun1', stmt='fun1()')
t.timeit()
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"程序运行时间:{run_time} 秒") # 输出结果:程序运行时间:3.3872999000595883 秒
2、使用生成器优化代码
注:为了方便以下内容皆省略主函数main的代码部分
def fun1():
list=[ i for i in range(100)] # 程序运行时间:2.1053185999626294 秒
从上述的推导式程序中可以看出,除了理解更简洁、更容易阅读之外,它也更快。这使得此方法成为生成列表和循环的首选方法。
三、避免字符串连接,使用join()
join()
是一个字符串方法,在Python中用于将序列中的元素连接(或拼接)成一个字符串,通常使用特定的分隔符。他的优点通常为:
- 1. 效率高:
join()
是连接字符串的高效方法,尤其是当处理大量字符串时,它通常比使用+
操作符或%
格式化更快,在连接大量字符串时,join()
方法通常比逐个连接更节省内存。 - 2. 简洁性:
join()
使得代码更加简洁,避免了重复的字符串连接操作。 - 3. 灵活性:可以指定任何字符串作为分隔符,这为字符串拼接提供了极大的灵活性。
- 4. 广泛的用途:不仅可以用于字符串,还可以用于列表、元组等序列类型,只要元素可以被转换成字符串。
举例:
def fun1():
obj=['hellow','my','name','is','xiaoyu','!']
s=""
for i in obj:
s+=i # 程序运行时间:0.3610708999913186 秒
使用 join()
来实现字符串拼接:
def fun1():
obj=['hellow','my','name','is','xiaoyu','!']
"".join(obj) # 程序运行时间:0.18804279994219542 秒
使用join()
将函数的执行时间从0.36秒减少到0.18秒。
四、使用Map代替循环
在多数场景中,传统的for循环可以被更为高效的map()函数所替代。map()是一个Python内置的高阶函数,它能够将指定的函数应用于各种可迭代的数据结构,如列表、元组或字符串。使用map()的主要优势在于,它提供了一种更为简洁且高效的数据处理方式,避免了编写显式的循环代码。
传统的循环方式:
def fun1():
arr=["hello", "my", "name", "is", "xiaoyu", "!"]
new = []
for i in arr:
new.append(i) # 程序运行时间:0.31288250000216067 秒
使用map()
函数做相同的功能:
def fun2(x):
return x
def fun1():
arr=["hello", "my", "name", "is", "xiaoyu", "!"]
map(fun2,arr) # 程序运行时间:0.18387670000083745 秒
对比之后,使用map()
节省了将近一半的时间,大大提升了运行效率
五、选择正确的数据结构
选用恰当的数据结构对提升Python代码的执行效率至关重要。各类数据结构都针对特定操作进行了优化,合理选择能够加速数据的检索、添加和移除过程,进而增强程序的整体运行效能。
例如,判断容器内的元素的时候,字典的查找效率高于列表,但是是在大量数据的情况下,少量数据恰恰相反
# 使用少量数据进行测试
def fun1():
arr=["hello", "my", "name", "is", "xiaoyu", "!"]
'hello' in arr
'my' in arr # 程序运行时间:0.11527379998005927 秒
def fun1():
arr={"hello", "my", "name", "is", "xiaoyu", "!"}
'hello' in arr
'my' in arr # 程序运行时间:0.17057139997836202 秒
# 使用 numpy 进行随机生成100个整数
def fun1():
nums = {i for i in np.random.randint(100, size=100)}
1 in nums # 程序运行时间:14.48330469999928 秒
def fun1():
nums = {i for i in np.random.randint(100, size=100)}
1 in nums # 程序运行时间:13.411826699972153 秒
看到了在少量数据的情况下list执行效率是要大于dict的,但是在大量数据的情况下,dict的效率大于list
如果有频繁的新增、删除操作,新增、删除的元素数量又很多时,list的效率不高。此时,应该考虑使用collections.deque
。collections.deque
是双端队列,同时具备栈和队列的特性,能够在两端进行 O(1)
复杂度的插入和删除操作。
collections.deque
的使用
from collections import deque
def fun1():
arr=deque()# 创建一个空的deque
for i in range(1000000):
arr.append(i)
# 程序运行时间:0.05507110000002058 秒
def fun1():
arr=[]
for i in range(1000000):
arr.append(i)
# 程序运行时间:0.06128990000001977 秒
list
的查找操作也非常耗时。当需要在list
频繁查找某些元素,或频繁有序访问这些元素时,可以使用bisect
维护list
对象有序并在其中进行二分查找,提升查找的效率。
六、避免不必要的函数调用
在Python编程中,优化函数调用次数对于提升代码效率至关重要。过多的函数调用不仅增加了开销,还可能消耗额外的内存,从而拖慢程序的运行速度。为了提升性能,我们应尽量减少不必要的函数调用,并尝试将多个操作合并成一个,以此来减少执行时间和资源消耗。这样的优化策略有助于我们编写更高效、更快速的代码。
七、避免不必要的import
虽然Python的import
语句相对较快,但每个import
都会涉及到查找模块、执行模块代码(如果还没有被执行过)、并将模块对象放入到当前命名空间中。这些操作都需要一定的时间和内存。当你不必要地导入模块时,就会增加这些开销。
八、避免使用全局变量
import math
size=10000
def fun1():
for i in range(size):
for j in range(size):
z = math.sqrt(i) + math.sqrt(j)
# 程序运行时间:15.630933800013736
许多程序员刚开始会用 Python 语言写一些简单的脚本,当编写脚本时,通常习惯了直接将其写为全局变量,例如上面的代码。但是,由于全局变量和局部变量实现方式不同,定义在全局范围内的代码运行速度会比定义在函数中的慢不少。通过将脚本语句放入到函数中,通常可带来 15% - 30% 的速度提升。
import math
def fun1():
size = 10000
for i in range(size):
for j in range(size):
z = math.sqrt(i) + math.sqrt(j)
# 程序运行时间:14.933845699997619 秒
九、避免模块和函数属性访问
import math # 不推荐写法
def fun2(size: int):
result = []
for i in range(size):
result.append(math.sqrt(i))
return result
def fun1():
size = 10000
for _ in range(size):
result = fun2(size)
# 程序运行时间:10.154493000009097 秒
每次使用.
(属性访问操作符时)会触发特定的方法,如__getattribute__()
和__getattr__()
,这些方法会进行字典操作,因此会带来额外的时间开销。通过from import
语句,可以消除属性访问。
from math import sqrt # 推荐写法:用到哪个模块就导哪个模块
def fun2(size: int):
result = []
for i in range(size):
result.append(sqrt(i))
return result
def fun1():
size = 10000
for _ in range(size):
result = fun2(size)
# 程序运行时间:8.960758000030182 秒
十、减少内层for
循环的计算
import math
def fun1():
size = 10000
sqrt = math.sqrt
for x in range(size):
for y in range(size):
z = sqrt(x) + sqrt(y) # sqrt() 求非负实数的平方根
# 程序运行时间:14.267008299939334 秒
在上面代码中sqrt(x)
位于内测for
循环,每次循环都会重新计算,增加不必要的时间开销
import math
def fun1():
size = 10000
sqrt = math.sqrt
for x in range(size):
sqrt_x=sqrt(x) # 在外层for循环进行计算
for y in range(size):
z = sqrt_x + sqrt(y)
# 程序运行时间:8.499037600005977 秒
总结
通过这些方法,我们可以有效地提高Python代码的性能,使其在处理复杂任务时更加快速和高效。记住,性能优化是一个持续的过程,需要根据具体情况不断调整和改进,python运行速度的优化方法不限于以上方法,还有很多,如有大佬路过,请多指教。