本文首发于公众号【蟒猫】,未经授权请勿转载。原文地址:https://mp.weixin.qq.com/s/mK1nav2vKykZaKw_TY-rtwPython的内置函数sum()可以接收两个参数,当第一个参数为二维列表时,第二个参数为一维列表dimensionallist时,可以达到列表降维的效果。在之前的文章《如何给列表降维?sum()函数的妙用》中,我们介绍了这个用法,同时也对sum()函数做了扩展研究。那篇文章发表后,毛兄得到了一些宝贵的反馈,不仅增长了见识,也启发了他的思维能力。因此,我决定再写一篇,继续和大家聊聊sum()函数和列表降维。如果你看完后有所启发,欢迎留言与我交流。有同学表示没想到sum()函数可以这么用,长知识了!毛哥第一次在交流群里看到这个用法的时候也是这么想的。很高兴在完成它作为一篇文章后得到别人的认可。学习新东西,分享它,最终让读者受益,这启发了我——应该每天进步并分享我所学到的东西。有同学已经知道sum()的用法,并指出其性能不好,不推荐使用。这是我从来没有考虑过的事情,但我必须认真对待。没错,sum()函数在列表降维方面有奇效,但性能堪忧,并不是最佳选择。因此,本文想继续讨论的话题是:(1)sum()函数的性能到底差了多少,为什么会差?(2)既然sum()不是最好的列表降维方法,那么有什么替代方法吗?在stackoverflow网站上,有人问了“Howtomakeaflatlistoutoflistsoflists”这个问题,这正是我们在上一篇文章中提出的问题。答案中有人分析了7种方法的时间表现。先看测试代码:importfunctoolsimportitertoolsimportnumpyimportoperatorimportperfplotdefforfor(a):return[itemforsublistinaforiteminsublist]defsum_brackets(a):returnsum(a,[])deffunctools_reduce(a):返回functools.reduce(operator.concat,a)deffunctools_reduce_iconcat(a):返回functools.reduce(operator.iconcat,a,[])defitertools_chain(a):返回列表(itertools.chain.from_iterable(a))defnumpy_flat(a):返回列表(numpy.array(a).flat)defnumpy_concatenate(a):返回列表(numpy.concatenate(a))perfplot.show(setup=lambdan:[list(range(10))]*n,kernels=[forfor,sum_brackets,functools_reduce,functools_reduce_iconcat,itertools_chain,numpy_flat,numpy_concatenate],n_range=[2**kforkinrange(16)],logx=True,logy=True,xlabel='numlists')代码包含了最有代表性的7种解法,使用perfplot(注意:这是测试者自己开发的库)进行可视化,结果非常直观的展示了随着数据量的增加,这些的效率方法各不相同。从测试图可以看出,当数据量小于10时,sum()函数的效率非常高,但随着数据量的增加,耗时急剧增加,远超其他方法损失。值得注意的是,functools_reduce方法的性能曲线几乎与sum_brackets重合。在另一个回答中,也有人对7种方法做了性能测试(巧的是,使用的可视化库也是测试者自己开发的)。其中,functools.reduce结合了lambda函数,虽然写法不同,但其时间效率与sum()函数基本一致:Iterableimportoperatorfromiteration_utilitiesimportdeepflattendefnested_list_comprehension(lsts):return[itemlsfortssubiteinminsublist]defitertools_chain_from_iterable(lsts):返回列表(chain.from_iterable(lsts))defpythons_sum(lsts):returnsum(lsts,[])defreduce_add(lsts):returnreduce(lambdax,y:x+y,lsts)defpylangs_flatten(lsts):returnlist(flatten(lsts))defflatten(items):"""Yielditemsfromanynestediterable;参见REF."""forxinitems:ifisinstance(x,Iterable)andnotisinstance(x,(str,bytes)):yieldfromflatten(x)else:yieldxdefreduce_concat(lsts):returnreduce(operator.concat,lsts)defiteration_utilities_deepflatten(lsts):returnlist(deepflatten(lsts,depth=1))fromsimple_benchmarkimportbenchmarkb=benchmark([nested_list_comprehension,itertools_chain_from_iterable,pythons_sum,reduce_add,pylangs_flatten,reduce_concat,iteration_utilities=[deepflatenments]{0]*5]*(2**i)foriinrange(1,13)},argument_name='numberofinnerlists')b.plot()这证实了两点:sum()函数确实执行令人担忧;它的执行效果其实就是将每个子列表一个一个地相加(concat)那么,问题来了,到底是什么原因导致sum()函数的性能变慢呢?在它的实现源代码中,我发现了一条评论:/*在这里使用PyNumber_InPlaceAdd而不是PyNumber_Add很诱人,以避免在执行'sum(list_of_lists,[])'时的二次运行时间。但是,这会导致行为发生变化:像empty=[]sum([[x]forxinrange(10)],empty)这样的代码片段会改变empty的值。*/为了不改变sum()函数的第二个参数的值,CPython没有使用in-place取而代之的是加法(PyNumber_InPlaceAdd),而是使用了比较消耗性能的普通加法(PyNumber_Add)。这种方法所花费的时间是二次运行时间。为什么要在这里牺牲性能?我猜想(只是粗浅的猜测),可能有两个考虑,一个是为了第二个参数(start)的一致性,因为它通常是一个值,是一个不可变对象,所以当它是一个可变对象类型时,它最好不要修改;其次,为了保证sum()函数是一个纯函数,以便多次执行时返回相同的结果。于是,我继续追问:哪种方法最好?总体来说,当子列表数小于10时,sum()函数几乎是最优的,这和一些方法没有太大区别。但是,当子列表数量增加时,最佳选择是functools.reduce(operator.iconcat,a,[]),其次是list(itertools.chain.from_iterable(a))。实际上,最优解中的iconcat(a,b)等价于a+=b,是一种原地修改的方法。operator.iconcat(a,b)operator.__iconcat__(a,b)a=iconcat(a,b)等同于a+=b对于a和b序列。出于一致性原因,这正是sum()函数,并丢弃实施计划。至此,上面提出的所有问题都得到了解答。最后总结一下:sum()函数使用了非原地修改加法。当用于列表降维时,随着数据量的增加,其性能会随着二次方程式急剧增加,因此性能堪忧;而reduce结合iconcat的方法是数据量大的时候最好的解决方案。这个结果和你想的一致吗?希望本文的分享能给大家带来新的收获。相关链接:如何对列表进行降维?sum()函数的神奇之处:https://mp.weixin.qq.com/s/cr_noDx6s1sZ6Xt6PDpDVQstackoverflow问题:https://stackoverflow.com/questions/952914/how-to-make-a-flat-list-out-of-list-of-lists公众号【Python猫】,本号连载系列精品文章,喵星哲学猫系列、Python进阶系列、好书推荐系列、技术写作、精品英文推荐和翻译等,欢迎关注。后台回复“爱学习”,免费领取学习大礼包。
