本文原創並首發於公衆號【Python貓】,未經受權,請勿轉載。python
原文地址:mp.weixin.qq.com/s/mK1nav2vK…bash
Python 的內置函數 sum() 能夠接收兩個參數,當第一個參數是二維列表,第二個參數是一維列表的時候,它能夠實現列表降維的效果。函數
在上一篇《如何給列表降維?sum()函數的妙用》中,咱們介紹了這個用法,還對 sum() 函數作了擴展的學習。性能
那篇文章發佈後,貓哥收到了一些頗有價值的反饋,不只在知識面上得到了擴充,在思惟能力上也獲得了一些啓發,所以,我決定再寫一篇文章,繼續跟你們聊聊 sum() 函數以及列表降維。若你讀後有所啓發,歡迎留言與我交流。學習
有些同窗表示,沒想到 sum() 函數居然能夠這麼用,漲見識了!貓哥最初在交流羣裏看到這種用法時,也有一樣的想法。整理成文章後,能獲得別人的承認,我很是開心。測試
學到新東西,進行分享,最後令讀者也有所獲,這鼓舞了我——應該每日精進,並把所學分享出去。大數據
也有的同窗早已知道 sum() 的這個用法,還指出它的性能並很差,不建議使用。這是我未曾考慮到的問題,但又不得不認真對待。網站
是的,sum() 函數作列表降維有奇效,但它性能堪憂,並非最好的選擇。ui
所以,本文想繼續探討的話題是:(1)sum() 函數的性能到底差多少,爲何會差?(2)既然 sum() 不是最好的列表降維方法,那是否有什麼替代方案呢?this
在 stackoverflow
網站上,有人問了個「How to make a flat list out of list of lists」問題,正是咱們在上篇文章中提出的問題。在回答中,有人分析了 7 種方法的時間性能。
先看看測試代碼:
import functools
import itertools
import numpy
import operator
import perfplot
def forfor(a):
return [item for sublist in a for item in sublist]
def sum_brackets(a):
return sum(a, [])
def functools_reduce(a):
return functools.reduce(operator.concat, a)
def functools_reduce_iconcat(a):
return functools.reduce(operator.iconcat, a, [])
def itertools_chain(a):
return list(itertools.chain.from_iterable(a))
def numpy_flat(a):
return list(numpy.array(a).flat)
def numpy_concatenate(a):
return list(numpy.concatenate(a))
perfplot.show(
setup=lambda n: [list(range(10))] * n,
kernels=[
forfor, sum_brackets, functools_reduce, functools_reduce_iconcat,
itertools_chain, numpy_flat, numpy_concatenate
],
n_range=[2**k for k in range(16)],
logx=True,
logy=True,
xlabel='num lists'
)
複製代碼
代碼囊括了最具表明性的 7 種解法,使用了 perfplot
(注:這是該測試者本人開發的庫)做可視化,結果很直觀地展現出,隨着數據量的增長,這幾種方法的效率變化。
從測試圖中可看出,當數據量小於 10 的時候,sum() 函數的效率很高,可是,隨着數據量增加,它所花的時間就出現劇增,遠遠超過了其它方法的損耗。
值得注意的是,functools_reduce 方法的性能曲線幾乎與 sum_brackets 重合。
在另外一個回答中,有人也作了 7 種方法的性能測試(巧合的是,所用的可視化庫也是測試者本身開發的),在這幾種方法中,functools.reduce 結合 lambda 函數,雖然寫法不一樣,它的時間效率與 sum() 函數也基本重合:
from itertools import chain
from functools import reduce
from collections import Iterable # or from collections.abc import Iterable
import operator
from iteration_utilities import deepflatten
def nested_list_comprehension(lsts):
return [item for sublist in lsts for item in sublist]
def itertools_chain_from_iterable(lsts):
return list(chain.from_iterable(lsts))
def pythons_sum(lsts):
return sum(lsts, [])
def reduce_add(lsts):
return reduce(lambda x, y: x + y, lsts)
def pylangs_flatten(lsts):
return list(flatten(lsts))
def flatten(items):
"""Yield items from any nested iterable; see REF."""
for x in items:
if isinstance(x, Iterable) and not isinstance(x, (str, bytes)):
yield from flatten(x)
else:
yield x
def reduce_concat(lsts):
return reduce(operator.concat, lsts)
def iteration_utilities_deepflatten(lsts):
return list(deepflatten(lsts, depth=1))
from simple_benchmark import benchmark
b = benchmark(
[nested_list_comprehension, itertools_chain_from_iterable, pythons_sum, reduce_add,
pylangs_flatten, reduce_concat, iteration_utilities_deepflatten],
arguments={2**i: [[0]*5]*(2**i) for i in range(1, 13)},
argument_name='number of inner lists'
)
b.plot()
複製代碼
這就證明了兩點:sum() 函數確實性能堪憂;它的執行效果實際是每一個子列表逐一相加(concat)。
那麼,問題來了,拖慢 sum() 函數性能的緣由是啥呢?
在它的實現源碼中,我找到了一段註釋:
/* It's tempting to use PyNumber_InPlaceAdd instead of PyNumber_Add here, to avoid quadratic running time when doing 'sum(list_of_lists, [])'. However, this would produce a change in behaviour: a snippet like empty = [] sum([[x] for x in range(10)], empty) would change the value of empty. */ 複製代碼
爲了避免改變 sum() 函數的第二個參數值,CPython 沒有采用就地相加的方法(PyNumber_InPlaceAdd),而是採用了較耗性能的普通相加的方法(PyNumber_Add)。這種方法所耗費的時間是二次方程式的(quadratic running time)。
爲何在這裏要犧牲性能呢?我猜測(只是淺薄猜想),可能有兩種考慮,一是爲了第二個參數(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) is equivalent to a += b for a and b sequences.
複製代碼
這正是 sum() 函數出於一致性考慮,而捨棄掉的實現方案。
至此,前文提出的問題都找到了答案。
我最後總結一下吧:sum() 函數採用的是非就地修改的相加方式,用做列表降維時,隨着數據量增大,其性能將是二次方程式的劇增,因此說是性能堪憂;而 reduce 結合 iconcat 的方法,纔是大數據量時的最佳方案。
這個結果是否與你所想的一致呢?但願本文的分享,能給你帶來新的收穫。
相關連接:
如何給列表降維?sum()函數的妙用 :mp.weixin.qq.com/s/cr_noDx6s…
stackoverflow 問題:stackoverflow.com/questions/9…
公衆號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫做、優質英文推薦與翻譯等等,歡迎關注哦。後臺回覆「愛學習」,免費得到一份學習大禮包。