Python代码性能优化的综合指南

Python代码性能优化的综合指南

    • 计算Python脚本的执行时间
    • I. I/O密集型操作
      • I/O密集型操作的优化方法
    • II. 使用生成器生成列表和字典
      • 1. 传统方法
      • 2. 使用生成器优化代码
    • III. 避免字符串拼接,使用`join()`
    • IV. 使用`map()`替换循环
      • 传统循环方法
      • 使用`map()`函数实现相同功能
    • V. 选择合适的数据结构
      • 少量数据测试
      • `collections.deque`的使用方法
    • VI. 避免不必要的函数调用
    • VII. 避免不必要的`import`
    • VIII. 避免使用全局变量
    • IX. 避免模块和函数的属性访问
    • X. 减少内部`for`循环中的计算

Python作为一种动态类型的解释型语言,与C等静态类型的编译型语言相比,执行速度可能会较慢。然而,通过特定的技术和策略,我们可以显著提升Python代码的性能。

本文将探讨如何优化Python代码,使其运行得更快、更高效。我们将使用Python的timeit模块来精确测量代码的执行时间。

注意:默认情况下,timeit模块会将代码执行100万次,以确保测量结果的准确性和稳定性。

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

if __name__ == '__main__':
    # 执行print_hi('leapcell')方法
    t = timeit.Timer(setup='from __main__ import print_hi', stmt='print_hi("leapcell")')
    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. I/O密集型操作

I/O密集型操作(Input/Output Intensive Operation)指的是执行时间大部分花费在等待输入输出操作完成的程序或任务。I/O操作包括从磁盘读取数据、向磁盘写入数据、网络通信等。这些操作通常涉及硬件设备,因此其执行速度受硬件性能和I/O带宽的限制。

它们的特征如下:

  • 等待时间:程序在执行I/O操作时,通常需要等待数据从外部设备传输到内存或从内存传输到外部设备,这可能导致程序执行被阻塞。
  • CPU利用率:由于I/O操作的等待时间,CPU在此期间可能处于空闲状态,导致CPU利用率较低。
  • 性能瓶颈:I/O操作的速度常常成为程序性能的瓶颈,尤其是在数据量大或传输速度慢的情况下。

例如,使用I/O密集型操作print,并执行100万次:

import time
import timeit

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

if __name__ == '__main__':
    start_time = time.perf_counter()
    # 执行print_hi('leapcell')方法
    t = timeit.Timer(setup='from __main__ import print_hi', stmt='print_hi("leapcell")')
    t.timeit()
    end_time = time.perf_counter()
    run_time = end_time - start_time
    print(f"程序执行时间: {run_time} 秒")

执行结果为3秒。

另一方面,如果不使用I/O操作,即不调用print(),而是调用一个空的print_hi('xxxx')方法,程序会显著加快:

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

I/O密集型操作的优化方法

在代码中需要I/O操作时,例如文件读写,可以使用以下方法提高效率:

  • 异步I/O:使用asyncio等异步编程模型,使程序在等待I/O操作完成时可以继续执行其他任务,提高CPU利用率。
  • 缓冲:使用缓冲区临时存储数据,减少I/O操作频率。
  • 并行处理:并行执行多个I/O操作,提高整体数据处理速度。
  • 数据结构优化:选择合适的数据结构,减少数据读写次数。

II. 使用生成器生成列表和字典

自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.363 秒

2. 使用生成器优化代码

注意:为了方便起见,以下内容省略了主函数main的代码部分。

def fun1():
    list=[ i for i in range(100)] 
    # 程序执行时间: 2.094 秒

从上述推导式程序可以看出,这种方法不仅更简洁易懂,而且速度更快。因此,这种方法已成为生成列表和循环时的首选方法。

III. 避免字符串拼接,使用join()

join()是Python的字符串方法,用于将序列中的元素连接(或合并)为字符串,通常使用特定的分隔符。其优点通常包括:

  • 高效性join()是字符串连接中的高效方法,尤其适用于大量字符串。通常比使用+运算符或%格式化更快。在连接大量字符串时,join()方法比逐个连接节省内存。
  • 简洁性join()使代码更简洁,避免了重复的字符串连接操作。
  • 灵活性:可以指定任意字符串作为分隔符,提供了字符串连接的灵活性。
  • 广泛适用性:不仅适用于字符串,还适用于列表、元组等序列类型。但元素必须能够转换为字符串。

例如:

def fun1():
    obj=['hello','this','is','leapcell','!']
    s=""
    for i in obj:
        s+=i       
    # 程序执行时间: 0.35186 秒

使用join()连接字符串时:

def fun1():
    obj=['hello','this','is','leapcell','!']
    "".join(obj) 
    # 程序执行时间: 0.1822 秒

使用join()后,函数的执行时间从0.35秒缩短到0.18秒。

IV. 使用map()替换循环

在大多数场景中,传统的for循环可以用更高效的map()函数替换。map()是Python内置的高阶函数,可以将指定的函数应用于列表、元组或字符串等可迭代数据结构。使用map()的主要优点是提供了更简洁、高效的数据处理方式,避免了显式的循环代码。

传统循环方法

def fun1():
    arr=["hello", "this", "is", "leapcell", "!"]
    new = []
    for i in arr:
        new.append(i) 
    # 程序执行时间: 0.3067 秒

使用map()函数实现相同功能

def fun2(x):
    return x

def fun1():
    arr=["hello", "this", "is", "leapcell", "!"]
    map(fun2,arr) 
    # 程序执行时间: 0.1875 秒

通过比较可以看出,使用map()可以节省近一半的时间,执行效率显著提高。

V. 选择合适的数据结构

选择合适的数据结构对于提高Python代码的执行效率至关重要。不同的数据结构针对特定操作进行了优化。合理的选择可以加快数据的查找、添加和删除速度,提升程序的整体操作效率。

例如,在判断容器中的元素时,字典的查找效率高于列表,但这适用于大量数据的情况。对于少量数据,情况则相反。

少量数据测试

def fun1():
    arr=["hello", "this", "is", "leapcell", "!"]
    'hello' in arr
    'my' in arr      
    # 程序执行时间: 0.1127 秒

def fun1():
    arr={"hello", "this", "is", "leapcell", "!"}
    'hello' in arr
    'my' in arr    
    # 程序执行时间: 0.1702 秒

使用numpy生成100个随机整数:

import numpy as np

def fun1():
    nums = {i for i in np.random.randint(100, size=100)}
    1 in nums    
    # 程序执行时间: 14.28 秒

def fun1():
    nums = {i for i in np.random.randint(100, size=100)}
    1 in nums    
    # 程序执行时间: 13.53 秒

可以看出,对于少量数据,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.0558 秒

def fun1():
    arr=[]
    for i in range(1000000):
        arr.append(i)
    # 程序执行时间: 0.06077 秒

list的查找操作也非常耗时。如果需要频繁查找list中的特定元素,或需要按顺序访问这些元素,可以使用bisect来维护list对象的顺序,并在其中进行二分查找,从而提高查找效率。

VI. 避免不必要的函数调用

在Python编程中,优化函数调用次数对提高代码效率至关重要。过多的函数调用不仅会增加开销,还可能导致额外的内存消耗,从而降低程序的执行速度。为了提高性能,应尽量减少不必要的函数调用,尝试将多个操作合并为一个。这样可以减少执行时间和资源消耗。这种优化策略有助于编写更高效、更快速的代码。

VII. 避免不必要的import

Python的import语句相对较快,但每次import都涉及模块查找、模块代码执行(如果尚未执行)以及将模块对象放入当前命名空间的操作。这些操作需要一定的时间和内存。不必要的模块导入会增加这些开销。

VIII. 避免使用全局变量

import math

size=10000
def fun1():
    for i in range(size):
        for j in range(size):
            z = math.sqrt(i) + math.sqrt(j) 
    # 程序执行时间: 15.6336 秒

许多程序员最初会用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.9319 秒

IX. 避免模块和函数的属性访问

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.1597 秒

每次使用.(属性访问运算符)时,都会触发__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.9682 秒

X. 减少内部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) 
    # 程序执行时间: 14.2634 秒

在上述代码中,sqrt(x)位于内部for循环中,每次循环都会重新计算,增加了不必要的时间开销。

import math

def fun1():
    size = 10000
    sqrt = math.sqrt
    for x in range(size):
        sqrt_x=sqrt(x) 
        for y in range(size):
            z = sqrt_x + sqrt(y) 
    # 程序执行时间: 8.4077 秒

通过将sqrt(x)移出内部循环,执行时间从14.26秒减少到8.40秒。


通过以上优化策略,我们可以显著提升Python代码的执行效率。

你可能感兴趣的:(LINUX,python,性能优化,开发语言)