设为首页 收藏本站
查看: 1127|回复: 0

[经验分享] 提高Python性能的一些建议

[复制链接]

尚未签到

发表于 2018-8-9 09:02:33 | 显示全部楼层 |阅读模式
  最近换住的地方,网费到期,有两个星期没更新博客了,博客还是要坚持写的,有时候工作时遇到了相关问题,查看相关博客,还是能够得到一些思路或者灵感。虽然写篇博客要话费不少时间(我一般要花一个半小时到两个小时之间),但是这中间码字呀、归纳总结的过程还是让我受益匪浅的,温故而知新!当然分享自己的学习心得,也会让自己认识一些志同道合的朋友,也挺好。不说许多,今天讲讲如何提高Python性能的问题。
  python的性能相对c语言等还是有一定的劣势,但是如果能掌握一些优化性能的技巧,不仅能够提高代码的运行效率,还能够使代码更加Pythonic。刚刚接触Python时,也在网上找了一些提高`python性能的博文看,还是另外受益匪浅,http://www.iyunv.net/article/56699.htm
  这篇博文还是写的不错的,可以参考。不过结合我自己最近看的书及书上的建议,做一个更细致的总结吧,不正之处欢迎批评指正,共同进步!
  一、循环优化的基本技巧
  循环的优化应遵循尽量减少循环过程中计算量的原则,多重循环的情况下尽量将内存的计算提到上一层。
  (1)减少循环内部计算。先看下面例子
#coding=utf-8  
import datetime
  
import math
  

  
#第一种方法
  
def fun1(iter,x):
  
    j = 0
  
    for i in iter:
  
        d = math.sqrt(x)
  
        j += i * d
  
    return j
  

  
#第二中方法
  
def fun2(iter,x):
  
    j = 0
  
    d = math.sqrt(x)
  
    for i in iter:
  
        j += i * d
  
    return j
  

  
iter = range(1,1000)
  
t0 = datetime.datetime.now()
  
a = fun1(iter,9)
  
t1 = datetime.datetime.now()
  
b = fun2(iter,9)
  
t2 = datetime.datetime.now()
  
print a,"  ",b
  
print t1-t0,t2-t1
  运行结果如下图:
DSC0000.png

  第二种方法比一种速度快,因为第一种方法中,d = math.sqrt(x)在循环内部,每次循环过程中都会重复计算一次,增加了系统开销,这里的d = math.sqrt(x)还是个比较简单的计算,如果遇到自己定义的复杂的、计算量大的那么第一种方法真的就不太好了。一般情况下,第二种方法比第一种方法运算效率高40%-60%。
  (2)将显示循环改为隐式循环。比如说,在求等差数列的和时,可以直接通过循环来计算,这个很简单,如下:
#coding=utf-8  
def SUM(n):
  
sum = 0
  
for i in xrange(n+1):
  
    sum +=i
  这么写是没问题的,但是等差数列有现成的求和公式呀,即n*(n+1)/2,没必要要再用循环了,所以说如果程序中有类似的情况,可以直接将显示循环改为隐式。
  (3)在循环中尽量引用局部变量。根据"LEGB"原则(不了解的可以参考之前的博文,地址:http://11026142.blog.51cto.com/11016142/1840128  ),在命名空间局部变量优先搜索,因此局部变量查询会比全局变量更快。在循环中,如果多次引用某一变量,要尽量将其转化为局部变量,看下面例子
#coding=utf-8  
import datetime
  
import math
  
x = [10,20,30,40,50,60,70,80,90]
  
#第一种方法
  
def fun1(x):
  
    for i in xrange(len(x)):
  
        x = math.sin(x)
  
    return x
  

  
#第二中方法
  
def fun2(x):
  
    loc_sin = math.sin
  
    for i in xrange(len(x)):
  
        x = loc_sin(x)
  
    return x
  

  
t0 = datetime.datetime.now()
  
a = fun1(x)
  
t1 = datetime.datetime.now()
  
b = fun2(x)
  
t2 = datetime.datetime.now()
  
print a
  
print b
  
print t1-t0,t2-t1
  运行结果如下:
DSC0001.png

  可以看到方法二快于方法一,我觉得这种方法,在平时应用某个库(包括标准库和自定义库,模块)的函数时,可以这样用,比较程序搜索这个库的函数也是需要时间,如果用第一种方法,那就是循环搜索了,那肯定会划分更多的时间,这个技巧我觉得值得大家去学习、借鉴。
  (4)对于嵌套循环,尽量将内层循环计算往上层移。看下面例子
#coding=utf-8  
import datetime
  

  
#第一种方法
  
def fun1(iter1,iter2):
  
    max1 = 0
  
    for i in range(len(iter1)):
  
        for j in range(len(iter2)):
  
            x = iter1 + iter2[j]
  
            max1 = x if x > max1 else max1
  
    return max1
  

  
#第二中方法
  
def fun2(iter1,iter2):
  
    max1 = 0
  
    for i in range(len(iter1)):
  
        temp = iter1
  
        for j in range(len(iter2)):
  
            x = temp + iter2[j]
  
            max1 = x if x > max1 else max1
  
    return max1
  
l1 = [1,23,4,5,34,8,10,18,42,10,6,88]
  
l2 = [100,102,34,15,16,56]
  
t0 = datetime.datetime.now()
  
a = fun1(l1,l2)
  
t1 = datetime.datetime.now()
  
b = fun2(l1,l2)
  
t2 = datetime.datetime.now()
  
print a
  
print b
  
print t1-t0,t2-t1
  运行结果如下:
DSC0002.png

  可见方法二的速度要快些,嵌套for循环的运行机制是i=0(以上面例子为例),然后j从0增到最大值,然后i自增1,j又从0增大到最大值,依次类推。所以在内层循环的上一层加个临时变量,内层使用是就不用重新计算。这里要说明一下,对于列表它的索引,切片等操作也是一种计算/运算,也是要花费时间的。
  二、使用不同的数据结构优化性能
  最常用的数据结构是list,它的内存管理类似于c++的vector,即先预分配一定数量的内存,当预分配的内存用完了但是不够用,又要继续往里插入元素,就会启动新一轮内存分配,list对象就会根据内存增长算法重新申请一块更大的内存空间,然后将原有的所有元素拷贝过去,销毁之前的内存,然后插入新元素,如果还不够用,继续重复上面的步骤。删除元素也是这样,如果发现已用空间比预分配内存空间的一半还少,list会申请一块小内存,再做一次拷贝,然后销毁大内存(如果想了解这部分知识,我推荐大家看看《Python源码剖析》这本书,这本书我也是一个月前开始看,目前还没看完,我觉得这本书写的很好,很值得读两三篇甚至四五篇,第一章与第二章内容要认真读,否则后面的东西越看越糊涂(大神除外))。
  可见,如果list对象经常有元素数量的巨大变化,而且比较频繁,这个时候应该考虑使用deque。如果不了解deque,可以参考我前面的博文http://11026142.blog.51cto.com/11016142/1851791
  deque是双端队列,同时具备栈和队列的特性。能够提供复杂度为O(1)的两端插入和删除操作。相对于list,它最大的优势在于内存管理。它申请的内存不够用时,不会像list那样,而是申请新的内存来容纳新的元素,然后将新元素与旧元素连接起来,避免了元素的拷贝。所以,但出现元素数量频繁出现巨大变化时,deque的性能是list的好几倍。
  array,中文名是数组,是一种序列数据结构,看起来和list很相似,但是所有成员必须是相同基本类型。array实例化时需要指明其存储元素类型。如'c',表示存储一个想当然c语言里的char类型,占用内存大小1字节。这就从另一个角度来说明,它可以优化代码的内存空间。看下面例子
#coding=utf-8  
import sys
  
import array
  
a = array.array('c',"hello,world")
  

  
c = list("hello,world")
  
print sys.getsizeof(a),sys.getsizeof(c)
  运行结果如下:
DSC0003.png

  明显,list对象更耗内存。这就会影响到一些其它操作的性能提升,比如将容器对象转换为字符串,在这一点上array性能高于list。看下面例子
#coding=utf-8  
import array
  
import datetime
  
a = array.array('c',"hello,world")
  
c = list("hello,world")
  
t0 = datetime.datetime.now()
  
s1 = ''.join(c)
  
t1= datetime.datetime.now()
  
s2 = ''.join(a)
  
t2 = datetime.datetime.now()
  
print t1 - t0,t2 - t1
  运行结果如下:
DSC0004.png

  也并不是所以的array性能提升比较大,比如排序,array性能不如list,看下例:
#coding=utf-8  
import array
  
import datetime
  
a = array.array('c',"hello,world")
  
c = list("hello,world")
  
t0 = datetime.datetime.now()
  
c.reverse()
  
t1= datetime.datetime.now()
  
a.reverse()
  
t2 = datetime.datetime.now()
  
print t1 - t0,t2 - t1
  结果如下:
DSC0005.png

  (三)利用好set的优势
  set是集合,python中集合是通过Hash算法实现的无序不重复元素集,创建集合是通过set()来实现的。看下图:
DSC0006.png

  set对象也支持添加元素,但它的性能是list添加元素性能的好几倍,这个不在此过多叙述,有时间专门写篇关于python集合的博文。
  set在求交集、并集、差集等与集合有关的操作,性能要逼list快,因此涉及到list的交集、并集、差集等运算,可以将list转换为set
  四、使用生成器提高效率
  生成器是Python中的一个高级用法,概念非常简单,如果一个函数中有yield语句,则称为生成器(generator),它是一种特殊的迭代器(iterator),也可以称为可迭代对象(iterable)。这三者关系如下图:
DSC0007.png

  如果你熟悉/了解Python的对象协议的话,最上面的那个是容器类协议,第二个是迭代器协议,如果你对这些不太熟悉,我觉得有必要找本经典教材来看看,我看的是《Python学习手册》,这本书还不错,介绍的比较详细,对于比较熟的内容,可以跳过。
  调用生成器函数会返回一个迭代器对象,只是这个迭代器对象是以生成器对象的形式出现的。看下面例子:
#coding=utf-8  
def fun(n):
  
    a,b = 1,1
  
    while a < n:
  
        yield a
  
        a,b = b,a + b
  

  
f = fun(10)
  
print type(f)
  
print dir(f)
  输出结果如下:
  <type 'generator'>
  ['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']
  可以看到它返回的是一个generator类型对象,而这个对象有__iter__()和__next__()方法,熟悉迭代器协议的都知道,迭代器协议就是要实现__iter__()和__next__()方法,可见它也是一个迭代器对象。
  生成器为什么能够提高运行效率?
  这是因为每一个生成器函数在调用之后,它的函数体并不执行,而是第一次调用next()的时候才会执行,仅在需要的时候产生对应的元素,而不是一次性生成所有的元素,从而节省了空间内存,提高了效率,理论上来讲,无限循环成为可能不会导致内存不够用的情况,这个在读数据处理的时候尤为重要。所以在循环过程中依次处理一个元素的时候,用生成器是最好的。
  五、使用多进程
  由于GIT的存在,是的Python中的多线程无法充分利用多核优势来提高运行效率,但是python提供了另外一个解决方案,多进程。 可以使用multiprocessing的Process,Pool等封装好的类,通过多进程的方式实现并行计算。由于篇幅有限,在此不过多介绍。
  另外还有一些其它的方法,比如推到式啊,借助一些其它的开发工具呀等等,这也是一个很值得去花时间去了解的知识,由于篇幅有限,无法详细叙述,后面我会慢慢尝试使用PyPy,Cython以及一些分析工具,然后将我的学习经验与大家分享。这些都是一些技巧,平时写代码的时候就要注意考虑进这些东西,多实践多总结,相信你会越来越优秀!

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-548940-1-1.html 上篇帖子: python编写api调用ceph对象网关 下篇帖子: Life is short,you need python!|(4)
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表