Python很慢?这十个方法让你的代码执行速度提升3倍!

前言

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. \1. 等待时间:程序在执行I/O操作时,往往需要等待数据从外部设备传输到内存,或从内存传输到外部设备,这会导致程序的执行被阻塞。
  2. \2. CPU利用效率:由于I/O操作的等待时间,CPU在这段时间内可能处于空闲状态,导致CPU利用率不高。
  3. \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

image-20240621223845875

而不使用i/o操作执行一个方法,即调用这个print_hi('xxxx')空方法,不使用print(),程序明显快了不少

def print_hi(name):
    # print(f'Hi, {name}')
    return

image-20240621115008544

如果代码中必要的时候,例如文件读写,可以使用如下方法提高效率

  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. \1. 效率高join() 是连接字符串的高效方法,尤其是当处理大量字符串时,它通常比使用 + 操作符或 % 格式化更快,在连接大量字符串时,join() 方法通常比逐个连接更节省内存。
  2. \2. 简洁性join() 使得代码更加简洁,避免了重复的字符串连接操作。
  3. \3. 灵活性:可以指定任何字符串作为分隔符,这为字符串拼接提供了极大的灵活性。
  4. \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.dequecollections.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运行速度的优化方法不限于以上方法,还有很多,如有大佬路过,请多指教。

posted @ 2024-09-18 15:31  白小雨  阅读(34)  评论(0编辑  收藏  举报