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的短路特性

# a为False时直接返回
if a and b:
# a为True时直接返回
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

由此可见,速度提升巨大

选择合适的数据结构

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内置数据类型时间复杂度

参考

python性能优化

原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/tech/python/275818.html

(0)
上一篇 2022年7月21日 11:41
下一篇 2022年7月21日 11:47

相关推荐

发表回复

登录后才能评论