Python 代码性能优化

hresh 501 0

Python 代码性能优化

前言

Python 的优劣势

Python 的优势

  1. 简单,优雅,易学
  2. 可移植性——由于 Python 开源的本质,它已经被移植到了大多数平台下面,例如:Windows、MacOS、Linux、Andorid、iOS 等等。
  3. 可扩展性——如果你需要你的一段关键代码运行得更快或者希望某些算法不公开,你可以把你的部分程序用 C 或 C++ 编写,然后在你的 Python 程序中使用它们。
  4. 可嵌入性——你可以把 Python 嵌入你的 C/C++ 程序,从而向你的程序用户提供脚本功能。
  5. 丰富的第三方库 Python 具有本身有丰富而且强大的库,而且由于 Python 的开源特性,第三方库也非常多,例如:在 web 开发、爬虫、科学计算等等

Python 的劣势

  1. 运行速度慢。
  2. 既是优点也是缺点,Python 的开源性使得 Python 语言不能加密,但是目前国内市场纯粹靠编写软件卖给客户的越来越少,网站和移动应用不需要给客户源代码,所以这个问题就是问题了。

当我们提到一门编程语言的效率时:通常有两层意思,第一是开发效率,这是对程序员而言,完成编码所需要的时间;另一个是运行效率,这是对计算机而言,完成计算任务所需要的时间。编码效率和运行效率往往是鱼与熊掌的关系,是很难同时兼顾的。不同的语言会有不同的侧重,Python 语言毫无疑问更在乎编码效率。所以网上流传着这么一句话“人生苦短,我用 Python”。

Python 为什么性能差

1、Python 是动态强类型语言

一个变量所指向对象的类型在运行时才确定,编译器做不了任何预测,也就无从优化。举一个简单的例子:r = a + b。 a 和 b 相加,但a和b的类型在运行时才知道,对于加法操作,不同的类型有不同的处理,所以每次运行的时候都会去判断a和b的类型,然后执行对应的操作。而在静态语言如 C++ 中,编译的时候就确定了运行时的代码。
  
2、Python 是解释性语言

边执行边解释,执行一句转化一句,但是不支持 JIT(just in time compiler)。

3、Python 中一切皆对象

Python 中一切都是对象,“everything is object”,包括类,类的实例,数字,模块。每个对象都需要维护引用计数,增加了额外的工作。

4、Python 中的 GIL

GIL 是 Python 最为诟病的一点,因为 GIL,Python 中的多线程并不能真正的并发。如果是在 IO BOUND 的业务场景,这个问题并不大,但是在 CPU BOUND 的场景,这就很致命了。一般都是使用多进程(pre fork),或者在加上协程。

任何 Python 线程执行前,必须先获得 GIL 锁,然后,每执行 100 条字节码,解释器就自动释放 GIL 锁,让别的线程有机会执行。这个 GIL 全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在 Python 中只能交替执行,即使 100 个线程跑在 100 核CPU上,也只能用到 1 个核。

5、垃圾回收

这个可能是所有具有垃圾回收的编程语言的通病。Python 采用引用计数、标记-清除和分代的垃圾回收策略,其中引用计数存在循环引用的缺陷。每次垃圾回收的时候都会中断正在执行的程序(stop the world),造成所谓的顿卡。

以上五点关于 Python 运行速度慢的原因,都是比较深层次的问题,目前都没有好的解决方案。为了提升代码运行速率,可以从代码优化层面入手,从而找到相应的解决方法。

Python 代码优化常见技巧

代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下使得程序的运行效率更高,根据 80/20 原则,实现程序的重构、优化、扩展以及文档相关的事情通常需要消耗 80% 的工作量。优化通常包含两方面的内容:减小代码的体积,提高代码的运行效率。

一个良好的算法能够对性能起到关键作用,因此性能改进的首要点是对算法的改进。在算法的时间复杂度排序上依次是:

O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)

因此如果能在时间复杂度上对算法进行一定的改进,将大大提升代码的性能。下面的内容将列出常用的一些优化技巧。

1、使用 dict 或 set 查找元素

Python 字典和集合中使用了 hash table,因此查找操作的复杂度为 O(1),而 list 实际是个数组,在 list 中,查找需要遍历整个 list,其复杂度为 O(n),因此对成员的查找访问等操作字典要比 list 更快。

from cProfile import Profile

def search():
    s_list = ['a', 'b', 'is', 'python', 'jason', 'hello', 'hill', 'with', 'phone', 'test',
            'dfdf', 'apple', 'pddf', 'ind', 'basic', 'none', 'baecr', 'var', 'bana', 'dd', 'wrd']

    #s_list = dict.fromkeys(s_list,True)
    #s_list = set(s_list)
    results = []

    for i in range(1000000):
        for s in ['is','hat','new','list','old','.']:
            if s not in s_list:
                results.append(s)

if __name__ == '__main__':
    prof = Profile()
    prof.runcall(search)
    prof.print_stats()

上述代码运行大概需要 2.010 seconds。如果去掉行 #list = dict.fromkeys(list,True) 的注释,将 list 转换为 dict 之后再运行,时间大约为 1.015 seconds,效率大概提高了一半。去掉行#s_list = set(s_list)的注释,同样效率提升了一半。因此在需要多数据成员进行频繁的查找或者访问的时候,使用 dict 或 set 是一个较好的选择。

2、使用 set 求交集、并集和差

set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的问题可以转换为 set 来操作。

交集、并集和差

from cProfile import Profile

def get_intersection():
    list_a = list(range(1000))
    list_b = list(range(1000))

    for i in range(1000):
        # intersection = []
        # for a in list_a:
        #     if a in list_b:
        #             intersection.append(a)
        intersection = list(set(list_a) & set(list_b))

def get_union():
    list_a = list(range(1000))
    list_b = list(range(1000))

    for i in range(1000):
        # union = []
        # union.extend(list_a)
        # for b in list_b:
        #     if b not in list_a:
        #         union.append(b)
        union = list(set(list_a) | set(list_b))

def get_difference():
    list_a = list(range(1000))
    list_b = list(range(1000))

    for i in range(1000):
        # difference = []
        # for a in list_a:
        #     if a not in list_b:
        #         difference.append(a)
        difference = list(set(list_a) - set(list_b))

def list_operation():
    get_intersection()
    get_union()
    get_difference()

if __name__ == "__main__":
    prof = Profile()
    prof.runcall(list_operation)
    prof.print_stats()

上述代码是通过 set 求解交集、并集和差,取消注释则是通过 list 求解交集、并集和差,在运行时间上看,set 方法更加高效。

3、优化循环

对循环的优化所遵循的原则是尽量减少循环过程中的计算量,循环之外能做的事不要放在循环内。

from cProfile import Profile

def loop_work():
    list_a = list(range(100))
    list_b = list(range(100))

    len_a = len(list_a)
    len_b = len(list_b)

    #循环优化前
    for i in range(10000):
        for i in range(len(list_a)):
            for j in range(len(list_b)):
                x = list_a[i] + list_b[j]
    #循环优化后
    # for i in range(10000):
    #     for i in range(len_a):
    #         x = list_a[i]
    #         for j in range(len_b):
    #             x += list_b[j]


if __name__ == '__main__':
    prof = Profile()
    prof.runcall(loop_work)
    prof.print_stats()

循环计算优化前耗时 57.130 seconds,优化后耗时 52.828 seconds。

4、优化包含多个判断表达式的顺序

对于and,应该把满足条件少的放在前面,对于or,把满足条件多的放在前面。

from cProfile import Profile

def or_work():
    nums = range(2000)

    for i in range(10000):
        #优化前
        new_nums = [x for x in nums if 10 < x < 20 or 1000 < x < 2000]
        # 优化后
        # new_nums = [x for x in nums if 1000 < x < 2000 or 100 < x < 20]

def and_work():
    nums = range(2000)

    for i in range(10000):
        # 优化前
        new_nums = [x for x in nums if x % 2 == 0 and x > 1900]
        # 优化后
        # new_nums = [x for x in nums if x > 1900 and x % 2 == 0]

def work():
    or_work()
    and_work()

if __name__ == '__main__':
    prof = Profile()
    prof.runcall(work)
    prof.print_stats()

优化后,消耗的时间缩短。

5、使用 join 合并迭代器中的字符串

在字符串连接的使用尽量使用 join() 而不是 +。

6、合理使用生成器(generator)

将列表生成式中的中括号改为小括号,即变为生成器。此外还可以利用 yield 创建 generator。

7、使用局部变量,避免"global" 关键字。python 访问局部变量会比全局变量要快得多,因 此可以利用这一特性提升性能。

8、使用 if is 进行判断

if done is not None 比语句 if done != None 更快

x = None

for i in range(1000000):
     #优化前
     # if x != None:
     #     key = "word"
     #优化后
     if x is not None:
         key = "word"

优化前耗时 0.097 seconds,优化后耗时 0.030 seconds,性能有所提升。

9、使用级联比较 "x < y < z" 而不是 "x < y and y < z"。

10、while 1 要比 while True 更快(当然后者的可读性更好)。

11、不借助中间变量交换两个变量的值

使用 a,b=b,a 而不是 c=a;a=b;b=c 来交换 a,b 的值,可以快 1 倍以上。

参考:

[1] https://www.cnblogs.com/xybaby/p/6510941.html

[2] https://www.ibm.com/developerworks/cn/linux/l-cn-python-optim/index.html#icomments

Python 优化窍门

另外还有关于 Python 优化的六个窍门,原文地址:https://blog.newrelic.com/engineering/python-performance-tips/

Python 是一种非常酷的语言,因为你可以用如此少的代码在如此短的时间内完成它。不仅如此,它还轻松支持许多任务,例如多处理。

Python 批评者有时声称 Python 很慢。但它不一定是这样:尝试这六个技巧来加速你的 Python 应用程序。

1、依靠关键代码的外部包

Python 简化了许多编程任务,但是对于一些时间敏感的任务,它的表现经常不尽人意。使用 C/C++ 或机器语言的外部功能包处理时间敏感任务,可以有效提高应用的运行效率。这些功能包往往依附于特定的平台,因此你要根据自己所用的平台选择合适的功能包。简而言之,这个窍门要你牺牲应用的可移植性以换取只有通过对底层主机的直接编程才能获得的运行效率。以下是一些你可以选择用来提升效率的功能包:

这些功能包的用处各有不同。比如说,使用 C 语言的数据类型,可以使涉及内存操作的任务更高效或者更直观。Pyrex 就能帮助 Python 延展出这样的功能。Pylnline 能使你在 Python 应用中直接使用 C 代码。内联代码是独立编译的,但是它把所有编译文件都保存在某处,并能充分利用 C 语言提供的高效率。

2、在排序时使用键

Python 含有许多古老的排序规则,这些规则在你创建定制的排序方法时会占用很多时间,而这些排序方法运行时也会拖延程序实际的运行速度。最佳的排序方法其实是尽可能多地使用键和内置的 sort()方法。譬如,拿下面的代码来说:

import operator
somelist = [(1, 5, 8), (6, 2, 4), (9, 7, 5)]
somelist.sort(key=operator.itemgetter(0))
somelist
#Output = [(1, 5, 8), (6, 2, 4), (9, 7, 5)]
somelist.sort(key=operator.itemgetter(1))
somelist
#Output = [(6, 2, 4), (1, 5, 8), (9, 7, 5)]
somelist.sort(key=operator.itemgetter(2))
somelist
#Output = [(6, 2, 4), (9, 7, 5), (1, 5, 8)],

在每段例子里,list 都是根据你选择的用作关键参数的索引进行排序的。这个方法不仅对数值类型有效,还同样适用于字符串类型。

3、优化循环

每一种编程语言都强调最优化的循环方案。当使用 Python时,你可以借助丰富的技巧让循环程序跑得更快。然而,开发者们经常遗忘的一个技巧是:尽量避免在循环中访问变量的属性 。譬如,拿下面的代码来说:

lowerlist = ['this', 'is', 'lowercase']
upper = str.upper
upperlist = []
append = upperlist.append
for word in lowerlist:
    append(upper(word))
    print(upperlist)
    #Output = ['THIS', 'IS', 'LOWERCASE']

每次你调用 str.upper, Python 都会计算这个式子的值。然而,如果你把这个求值赋值给一个变量,那么求值的结果就能提前知道,Python 程序就能运行得更快。因此,关键就是尽可能减小 Python 在循环中的工作量。因为 Python 解释执行的特性,在上面的例子中会大大减慢它的速度。

(注意:优化循环的方法还有很多,这只是其中之一。比如,很多程序员会认为,列表推导式是提高循环速度的最佳方法。关键在于,优化循环方案是提高应用程序运行速度的上佳选择。)

4、使用较新的版本

如果你在网上搜索 Python,你会发现数不尽的信息都是关于如何升级 Python 版本。通常,每个版本的 Python 都会包含优化内容,使其运行速度优于之前的版本。但是,限制因素在于,你最喜欢的函数库有没有同步更新支持新的 Python 版本。与其争论函数库是否应该更新,关键在于新的 Python 版本是否足够高效来支持这一更新。

你要保证自己的代码在新版本里还能运行。你需要使用新的函数库才能体验新的 Python 版本,然后你需要在做出关键性的改动时检查自己的应用。只有当你完成必要的修正之后,你才能体会新版本的不同。

然而,如果你只是确保自己的应用在新版本中可以运行,你很可能会错过新版本提供的新特性。一旦你决定更新,请分析你的应用在新版本下的表现,并检查可能出问题的部分,然后优先针对这些部分应用新版本的特性。只有这样,用户才能在更新之初就觉察到应用性能的改观。

5、尝试多种编码方法

每次创建应用时都使用同一种编码方法几乎无一例外会导致应用的运行效率不尽人意。可以在程序分析时尝试一些试验性的办法。譬如说,在处理字典中的数据项时,你既可以使用安全的方法,先确保数据项已经存在再进行更新,也可以直接对数据项进行更新,把不存在的数据项作为特例分开处理。请看下面第一段代码:

n = 16
myDict = {}
for i in range(0, n):
    char = 'abcd'[i%4]
    if char not in myDict:
        myDict[char] = 0
        myDict[char] += 1
        print(myDict)

当一开始 myDict 为空时,这段代码会跑得比较快。然而,通常情况下,myDict 填满了数据,至少填有大部分数据,这时换另一种方法会更有效率。

    n = 16
    myDict = {}
    for i in range(0, n):
        char = 'abcd'[i%4]
        try:
            myDict[char] += 1
        except KeyError:
            myDict[char] = 1
        print(myDict)

在两种方法中输出结果都是一样的。区别在于输出是如何获得的。跳出常规的思维模式,创建新的编程技巧能使你的应用更有效率。

6、交叉编译你的应用

开发者有时会忘记计算机其实并不理解用来创建现代应用程序的编程语言。计算机理解的是机器语言。为了运行你的应用,你借助一个应用将你所编的人类可读的代码转换成机器可读的代码。有时,你用一种诸如 Python 这样的语言编写应用,再以 C++这样的语言运行你的应用,这在运行的角度来说,是可行的。关键在于,你想你的应用完成什么事情,而你的主机系统能提供什么样的资源。

Nuitka 是一款有趣的交叉编译器,能将你的 Python 代码转化成 C++代码。这样,你就可以在 native 模式下执行自己的应用,而无需依赖于解释器程序。你会发现自己的应用运行效率有了较大的提高,但是这会因平台和任务的差异而有所不同。

在使用交叉编译器时,记得确保它支持你所用的Python版本。

交叉编译可能造成一些严重问题。比如,在使用 Nuitka 时,你会发现即便是一个小程序也会消耗巨大的驱动空间。因为 Nuitka 借助一系列的动态链接库(DDLs)来执行 Python 的功能。因此,如果你用的是一个资源很有限的系统,这种方法或许不太可行。

总结

前文所述的六个窍门都能帮助你创建运行更有效率的 Python 应用。但是 silver bullets 是不存在的。上述的这些窍门不一定每次都能奏效。在特定的 Python 的版本下,有的窍门或许比其他的表现更好,但这有时候甚至取决于平台的差异。你需要总结分析你的应用,找到它效率低下的部分,然后尝试这些窍门,找到解决问题的最佳方法。

发表评论 取消回复
表情 图片 链接 代码

分享