python 性能优化

简介

python相比较其他语言,在效率上会略有劣势,因此,我们在实现完功能后更应该去对python代码进行优化,减少不必要的消耗。

代码优化原则

  • 不要过早的去优化,首先保证一个正确的程序,再使程序变快 比 先是一个快的程序,再保证正确容易
  • 代码优化的代价,任何代码优化都需要时间和空间,因此代码优化的同时,可能是时间与空间性能的交换
  • 不要优化无关紧要的部分,尤其是逻辑较为复杂的。

代码优化实践

避免全局变量

start = time.time()
size = 10000000
for i in range(size):
    ret = i * i * i

print(time.time() - start)


def main():
    size = 10000000
    for i in range(size):
        ret = i * i * i


start = time.time()
main()
print(time.time() - start)
1.2879176139831543
0.7891571521759033

由上述可见,不使用全局变量的性能比使用全局变量的好了不少。

避免模块和函数属性被访问

尽量使用from module import module,减少import module,对于使用对象.函数,可以将对象.函数赋值给一个局部变量来提升性能,因为,在.的过程中会调用__getattr____getattribute

import math


def main():
    size = 10000000
    for i in range(size):
        ret = math.sqrt(i) + math.sqrt(i)


start = time.time()
main()
print(time.time() - start)

from math import sqrt


def main():
    size = 10000000
    for i in range(size):
        ret = sqrt(i) + sqrt(i)
2.371065139770508
2.194082021713257

由上述结果可以看出,使用了from module import module比之间import module性能稍好一些。

from math import sqrt


def main():
    size = 10000000
    l = []
    for i in range(size):
        l.append(sqrt(i) + sqrt(i))


start = time.time()
main()
print(time.time() - start)


def main():
    size = 10000000
    l = []
    append = l.append
    for i in range(size):
        append(sqrt(i) + sqrt(i))


start = time.time()
main()
print(time.time() - start)
2.809134006500244
2.6512084007263184

由此可见,性能稍好一些,但在部分场景下,之间.比赋值要快。

避免类内属性(频繁访问)的访问

类内属性包含self.value等,对于类中方法需要频繁获取对应self.value值时,建议将self.value赋值给一个局部变量,如下

import time


class Test:
    def __init__(self, num) -> None:
        self.num = num
        self.list = []

    def run(self):
        size = 100000000
        for i in range(size):
            self.list.append(self.num)


start = time.time()
Test(1).run()
print(time.time() - start)


class Test2:
    def __init__(self, num) -> None:
        self.num = num
        self.list = []

    def run(self):
        size = 100000000
        num = self.num
        for i in range(size):
            self.list.append(num)


start = time.time()
Test2(1).run()
print(time.time() - start)

10.327167272567749
8.838395833969116

相比较每次获取值,直接将对应值赋值给一个局部变量,性能稍好一些,但是对于类属性值需要频繁修改并且在其他方法中需要获取或者修改的情况,不建议使用局部变量进行操作。例如

import time


class Test:
    def __init__(self, num) -> None:
        self.num = num
        self.list = []

    def run(self):
        size = 10000000
        for i in range(size):
            self.list.append(self.num)
            self.num += 1

    def get_num(self):
        print(self.num)


start = time.time()
t = Test(1)
t.run()
t.get_num()
print(time.time() - start)


class Test2:
    def __init__(self, num) -> None:
        self.num = num
        self.list = []

    def run(self):
        size = 10000000
        num = self.num
        for i in range(size):
            self.list.append(num)
            num += 1

    def get_num(self):
        print(self.num)


start = time.time()
t = Test2(1)
t.run()
t.get_num()
print(time.time() - start)

10000001
1.577972650527954
1
1.2627947330474854

由上述结果看出,虽然性能有所提升,但对于类属性的修改没有生效。

避免使用不必要的抽象(处理)

尽量减少使用不必要的处理层,例如装饰器、属性访问、描述器等,例如对于类内属性的访问,是否有必要设置settergetter方法

避免过多的使用没必要的数据复制

对于数据复制由深拷贝和浅拷贝,尽量减少滥用深拷贝。

减少使用中间变量的使用

start = time.time()
a = 1
b = 2
temp = a
a = b
b = temp
print(time.time() - start)

start = time.time()
a = 1
b = 2
a, b = b, a
print(time.time() - start)

对于一个列表中有个字符串需要拼接,使用.join代替+

import time
import string
from typing import List


def concat_string(str_list: List[str]) -> str:
    result = ""
    for item in str_list:
        result += item
    return result


str_list = list(string.ascii_letters * 100000)
start = time.time()
concat_string(str_list)
print(time.time() - start)


def concat_string2(str_list: List[str]) -> str:
    return "".join(str_list)


start = time.time()
concat_string2(str_list)
print(time.time() - start)
1.418717861175537
0.03793454170227051

当使用a + b拼接字符串时,由于 Python 中字符串是不可变对象,其会申请一块内存空间,将a和b分别复制到该新申请的内存空间中。因此,如果要拼接n个字符串,会产生 n-1个中间结果,每产生一个中间结果都需要申请和复制一次内存,严重影响运行效率。而使用join()拼接字符串时,会首先计算出需要申请的总的内存空间,然后一次性地申请所需内存,并将每个字符串元素复制到该内存中去。

利用if的短路特性

# aFalse时直接返回
if a and b:
# aTrue时直接返回
if a or b:

上述的短路特性取决于判断条件的倾向性

使用for循环代替while循环

import time

size = 100000000
num = 1
start = time.time()
for i in range(size):
    num += 1
print(time.time() - start)

size = 100000000
num = 1
start = time.time()
while True:
    num += 1
    if num > size:
        break
print(time.time() - start)

7.680142641067505
8.76479697227478

使用隐式循环代替显式循环

例如使用range(size)代替for i in range(size)

减少内存循环的计算(如果对于外层循环的值不变的情况下)

import time
from math import sqrt

size = 10000

start = time.time()
for i in range(size):
    for j in range(size):
        num = sqrt(i) + sqrt(j)
print(time.time() - start)

start = time.time()
for i in range(size):
    sqrt_i = sqrt(i)
    for j in range(size):
        num = sqrt_i + sqrt(j)
print(time.time() - start)

27.43333649635315
21.37246870994568

使用numba.jit进行编译

需安装numba模块

pip install numba

numba可以将 Python 函数 JIT 编译为机器码执行,大大提高代码运行速度

import time
from math import sqrt
import numba

def main():
    size = 10000
    num = 1
    for i in range(size):
        sqrt_i = sqrt(i)
        for j in range(size):
            num = sqrt_i + sqrt(j)


start = time.time()
main()
print(time.time() - start)


@numba.jit
def main2():
    size = 10000
    num = 1
    for i in range(size):
        sqrt_i = sqrt(i)
        for j in range(size):
            num = sqrt_i + sqrt(j)


start = time.time()
main2()
print(time.time() - start)
14.618261098861694
0.21442890167236328

由此可见,速度提升巨大

在对于类中属性确定的情况下,使用__slot__函数指定类的实例属性

默认情况下,python对象用一个字典来保存实例属性,这样当创建很多对象时,会消耗较多内存,这因此需要使⽤__slots__来告诉Python不要使⽤字典,⽽且只给⼀个固定集合的属性分配空间。

选择合适的数据结构

Python 内置的数据结构如str, tuple, list, set, dict底层都是 C 实现的,速度非常快,自己实现新的数据结构想在性能上达到内置的速度几乎是不可能的。

list类似于 C++ 中的std::vector,是一种动态数组。其会预分配一定内存空间,当预分配的内存空间用完,又继续向其中添加元素时,会申请一块更大的内存空间,然后将原有的所有元素都复制过去,之后销毁之前的内存空间,再插入新元素。删除元素时操作类似,当已使用内存空间比预分配内存空间的一半还少时,会另外申请一块小内存,做一次元素复制,之后销毁原有大内存空间。

因此,如果有频繁的新增、删除操作,新增、删除的元素数量又很多时,list的效率不高。此时,应该考虑使用collections.dequecollections.deque是双端队列,同时具备栈和队列的特性,能够在两端进行 O(1)复杂度的插入和删除操作。

list的查找操作也非常耗时。当需要在list频繁查找某些元素,或频繁有序访问这些元素时,可以使用bisect维护list对象有序并在其中进行二分查找,提升查找的效率。

另外一个常见需求是查找极小值或极大值,此时可以使用heapq模块将list转化为一个堆,使得获取最小值的时间复杂度是O(1)。

当数据类型不确定时并且需要查找速度快的话,建议使用字典,因为字典底层是哈希表
python内置数据类型时间复杂度

如何知道当前代码的内存占用及执行时长?

可以使用ipython,支持Python 3.8+ and IPython 7.9+

pip install ipython_memory_usage

使用方法:
1.安装完成后在命令行输入ipython进入终端,输入如下:

import ipython_memory_usage.ipython_memory_usage as imu
imu.start_watching_memory()

2.输入完成后,再输入需要测试的代码,点击回车会自动显示执行时间及内存消耗,如下:

In [21]: class Test(object):
    ...:     def __init__(self, name, age) -> None:
    ...:         self.name = name
    ...:         self.age = age
    ...:     def run(self):
    ...:         print(f"run:{self.name}, {self.age}")
    ...: 
In [16] used 0.0703 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 103.02 MiB

In [22]: Test("tom", 13).run()
run:tom, 13
In [17] used 0.0156 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 103.04 MiB

In [23]: class Test2(object):
    ...:     __slot__ = ["name", "age"]
    ...: 
    ...:     def __init__(self, name, age) -> None:
    ...:         self.name = name
    ...:         self.age = age
    ...: 
    ...:     def run(self):
    ...:         print(f"run:{self.name}, {self.age}")
    ...: 
In [18] used 0.0000 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 103.04 MiB

In [24]: Test2("tom", 13).run()
run:tom, 13
In [19] used 0.0000 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 103.04 MiB

参考

python性能优化

posted @   形同陌路love  阅读(354)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示