[英]python list comprehension vs +=
今天我試圖找到一個方法,在python中對字符串進行一些處理。 一些比我說的高級程序員不使用+=
但是使用''.join()
我也可以在例如http://wiki.python.org/moin/PythonSpeed/#Use_the_best_algorithms_and_fastest_tools中讀到這個。 但是我自己測試了這個並且發現了一些奇怪的結果(這不是我想要再次猜測它們但是我想要堅持下去)。 我的想法是,如果有一個字符串"This is \\"an example text\\"
包含空格”,該字符串應轉換為Thisis"an example text"containingspaces
空格Thisis"an example text"containingspaces
空格被刪除,但僅在引號之外。
我使用''.join(list)
和一個使用+=
來測量我的算法的兩個不同版本的性能
import time
#uses '+=' operator
def strip_spaces ( s ):
ret_val = ""
quote_found = False
for i in s:
if i == '"':
quote_found = not quote_found
if i == ' ' and quote_found == True:
ret_val += i
if i != ' ':
ret_val += i
return ret_val
#uses "".join ()
def strip_spaces_join ( s ):
#ret_val = ""
ret_val = []
quote_found = False
for i in s:
if i == '"':
quote_found = not quote_found
if i == ' ' and quote_found == True:
#ret_val = ''.join( (ret_val, i) )
ret_val.append(i)
if i != ' ':
#ret_val = ''.join( (ret_val,i) )
ret_val.append(i)
return ''.join(ret_val)
def time_function ( function, data):
time1 = time.time();
function(data)
time2 = time.time()
print "it took about {0} seconds".format(time2-time1)
在我的機器上,這產生了這個輸出,對於使用+=
的算法有一個小優勢
print '#using += yields ', timeit.timeit('f(string)', 'from __main__ import string, strip_spaces as f', number=1000)
print '#using \'\'.join() yields ', timeit.timeit('f(string)', 'from __main__ import string, strip_spaces_join as f', number=1000)
與timeit計時時:
#using += yields 0.0130770206451
#using ''.join() yields 0.0108470916748
差別很小。 但是為什么''.join()
沒有明確地執行使用+=
的函數,但是對於'.join()版本似乎有一個小優勢。 我使用python-2.7.3在Ubuntu 12.04上測試了這個
比較算法時,請使用正確的方法; 使用timeit
模塊消除CPU利用率和交換的波動。
使用timeit
顯示兩種方法之間幾乎沒有區別,但''.join()
稍快一些:
>>> s = 1000 * string
>>> timeit.timeit('f(s)', 'from __main__ import s, strip_spaces as f', number=100)
1.3209099769592285
>>> timeit.timeit('f(s)', 'from __main__ import s, strip_spaces_join as f', number=100)
1.2893600463867188
>>> s = 10000 * string
>>> timeit.timeit('f(s)', 'from __main__ import s, strip_spaces as f', number=100)
14.545105934143066
>>> timeit.timeit('f(s)', 'from __main__ import s, strip_spaces_join as f', number=100)
14.43651008605957
函數中的大部分工作是循環遍歷每個字符並測試引號和空格,而不是字符串連接本身。 而且, ''.join()
變體做了更多工作; 您首先將元素附加到列表中(這將替換+=
字符串連接操作), 然后使用''.join()
在最后連接這些值。 而且這種方法仍然有點快。
您可能想要刪除正在進行的工作以僅比較連接部分:
def inplace_add_concatenation(s):
res = ''
for c in s:
res += c
def str_join_concatenation(s):
''.join(s)
這表現了:
>>> s = list(1000 * string)
>>> timeit.timeit('f(s)', 'from __main__ import s, inplace_add_concatenation as f', number=1000)
6.113742113113403
>>> timeit.timeit('f(s)', 'from __main__ import s, str_join_concatenation as f', number=1000)
0.6616439819335938
這表明''.join()
級聯還是很多的赫克快於+=
。 速度差異在於循環; s
在兩種情況下都是一個列表,但是''.join()
循環遍歷C中的值,而另一個版本必須在Python中循環它。 這在這里有所不同。
另一種選擇是編寫一個使用生成器連接的函數,而不是每次都附加到列表。
例如:
def strip_spaces_gen(s):
quote_found = False
for i in s:
if i == '"':
quote_found = not quote_found
if i == ' ' and quote_found == True:
# Note: you (c|sh)ould drop the == True, but I'll leave it here so as to not give an unfair advantage over the other functions
yield i
if i != ' ':
yield i
def strip_spaces_join_gen(ing):
return ''.join(strip_spaces_gen(ing))
對於較短的字符串,這似乎大致相同(作為連接):
In [20]: s = "This is \"an example text\" containing spaces"
In [21]: %timeit strip_spaces_join_gen(s)
10000 loops, best of 3: 22 us per loop
In [22]: %timeit strip_spaces(s)
100000 loops, best of 3: 13.8 us per loop
In [23]: %timeit strip_spaces_join(s)
10000 loops, best of 3: 23.1 us per loop
但對於較大的弦樂更快。
In [24]: s = s * 1000
In [25]: %timeit strip_spaces_join_gen(s)
100 loops, best of 3: 12.9 ms per loop
In [26]: %timeit strip_spaces(s)
100 loops, best of 3: 17.1 ms per loop
In [27]: %timeit strip_spaces_join(s)
100 loops, best of 3: 17.5 ms per loop
(這可能是OP已經知道的很多細節,但是完整地解決這個問題可以幫助那些最終解決這個問題的人)
mystring += suffix
的問題是字符串是不可變的,所以這實際上等同於mystring = mystring + suffix
。 因此,實現必須創建一個新的字符串對象,將mystring
所有字符復制到它,然后復制suffix
所有字符。 然后mystring
名稱被反彈以引用新字符串; mystring
引用的原始字符串對象不受影響。
就其本身而言,這實際上並不是一個問題。 連接這兩個字符串的任何方法都必須這樣做,包括''.join([mystring, suffix])
; 這實際上更糟糕 ,因為它必須首先構造一個列表對象然后迭代它,並且在拼接空字符串之間沒有實際的數據傳輸,在mystring
和suffix
之間需要至少一條指令進行整理。
當+=
成為問題時, 重復執行此操作。 像這樣的東西:
mystring = ''
for c in 'abcdefg' * 1000000:
mystring += c
請記住, mystring += c
等同於mystring = mystring + c
。 因此,在循環的第一次迭代中,它評估'' + 'a'
復制1個字符總數。 接下來它會'a' + 'b'
復制2個字符。 然后'ab' + 'c'
代表3個字符,然后'abc' + 'd'
代表4,我認為你可以看到它的發展方向。 每個后續的+=
重復前一個的所有工作,然后復制新的字符串。 這非常浪費。
''.join(...)
更好,因為你等到你知道要復制其中任何一個的所有字符串,然后將每個字符串直接復制到最終字符串對象中的正確位置。 與一些評論和答案所說的相反,即使您必須修改循環以將字符串附加到字符串列表,然后在循環之后將它們join
起來,這仍然是這種情況。 列表不是不可變的,因此附加到列表會在適當的位置修改它,並且它還只需要附加單個引用而不是復制字符串中的所有字符。 對列表執行數千次追加比執行數千次字符串+=
操作要快得多。
重復的字符串+=
理論上是一個問題,即使沒有循環,如果你只是編寫你的源代碼:
s = 'foo'
s += 'bar'
s += 'baz'
...
但實際上,除非所涉及的字符串非常龐大,否則你不太可能手動編寫足夠長的代碼序列。 所以請注意+=
in循環(或遞歸函數)。
當您嘗試計時時,您可能看不到此結果的原因是,在CPython解釋器中實際上對字符串+=
進行了優化。 讓我們回到我的愚蠢的示例循環:
mystring = ''
for c in 'abcdefg' * 1000000:
mystring += c
每次執行mystring = mystring + c
, mystring
的舊值變為垃圾並被刪除,名稱mystring
最終引用新創建的字符串,該字符串完全以舊對象的內容開頭。 我們可以通過識別mystring
即將成為垃圾來優化這一點,因此我們可以做任何我們喜歡的事情,而無需任何人關心。 因此,即使字符串在Python級別是不可變的,在實現級別我們也可以使它們動態擴展,我們將通過正常分配新的字符串和復制方法或通過擴展目標字符串來實現target += source
並且只復制源字符, 具體取決於target
是否會變成垃圾 。
這種優化的問題在於它很容易破壞。 它適用於小型自包含循環(順便說一句,它最容易轉換為使用join
)。 但是,如果你正在做一些更復雜的事情,並且你偶然會遇到多個字符串引用,那么代碼突然運行得慢得多。
假設您在循環中有一些日志記錄調用,並且日志記錄系統緩沖其消息一段時間以便一次打印它們(應該是安全的;字符串是不可變的)。 日志記錄系統中對字符串的引用可能會停止適用的+=
優化。
假設您已經將循環編寫為遞歸函數(Python無論如何都不喜歡,但仍然)出於某種原因使用+=
構建字符串。 外部堆棧幀仍將引用舊值。
或者也許你正在用字符串做的是生成一系列對象,所以你將它們傳遞給一個類; 如果類將字符串直接存儲在實例中,則優化會消失,但如果類首先操作它們,那么優化仍然有效。
從本質上講,看起來像一個非常基本的原始操作的性能要么是好的,要么是非常糟糕的,並且它取決於其他代碼而不是使用+=
的代碼。 在極端情況下,您可以更改一個完全獨立的文件(甚至可能是第三方軟件包),在您的一個模塊中引入一個長時間沒有變化的大規模性能回歸!
另外,我的理解是+=
優化只能在CPython上實現,因為它使用了引用計數; 您可以通過查看其引用計數來輕松判斷目標字符串何時是垃圾,而使用更復雜的垃圾收集,除非刪除引用並等待垃圾收集器運行,否則無法判斷。 為時已晚,無法決定如何實施+=
。 再說一次,非常簡單的基本代碼不應該在Python實現之間移植任何問題,當你將它移動到另一個實現時,它可能會突然運行得太慢而無法使用。
這里有一些基准測試來顯示問題的規模:
import timeit
def plus_equals(data):
s = ''
for c in data:
s += c
def simple_join(data):
s = ''.join(data)
def append_join(data):
l = []
for c in data:
l.append(c)
s = ''.join(l)
def plus_equals_non_garbage(data):
s = ''
for c in data:
dummy = s
s += c
def plus_equals_maybe_non_garbage(data):
s = ''
for i, c in enumerate(data):
if i % 1000 == 0:
dummy = s
s += c
def plus_equals_enumerate(data):
s = ''
for i, c in enumerate(data):
if i % 1000 == -1:
dummy = s
s += c
data = ['abcdefg'] * 1000000
for f in (
plus_equals,
simple_join,
append_join,
plus_equals_non_garbage,
plus_equals_maybe_non_garbage,
plus_equals_enumerate,
):
print '{:30}{:20.15f}'.format(f.__name__, timeit.timeit(
'm.{0.__name__}(m.data)'.format(f),
setup='import __main__ as m',
number=1
))
在我的系統上打印:
plus_equals 0.066924095153809
simple_join 0.013648986816406
append_join 0.086287975311279
plus_equals_non_garbage 540.663727998733521
plus_equals_maybe_non_garbage 0.731688976287842
plus_equals_enumerate 0.156824111938477
優化+=
工作得很好,當它工作(甚至擊敗啞append_join
通過一個小版本)。 我的數字表明你可能能夠通過在某些情況下用+=
替換append
+ join
來優化代碼,但是這樣做的好處是不值得其他一些未來變化意外得到井噴的風險(並且如果有的話,很可能會很小)循環中正在進行的任何其他實際工作;如果沒有,那么你應該使用類似simple_join
版本的東西)。
通過將plus_equals_maybe_non_garbage
與plus_equals_enumerate
進行比較,您可以看到,即使優化僅在每千次+=
操作中失敗一次,仍然會有5倍的性能損失。
+=
的優化實際上只是為了拯救那些沒有經驗的Python程序員,或者只是快速懶惰地編寫一些臨時代碼的人。 如果你正在考慮你在做什么,你應該使用join
。
總結:對於固定的少量連接,使用+=
是合適的。 join
總是使用循環建立串好。 實際上,由於+=
優化,您可能看不到從+=
join
代碼的巨大改進。 你仍然應該使用join
,因為優化是不可靠的,並且當它無法啟動時的差異可能是巨大的。
+=
和.join
之間的性能差異取決於很多因素:
操作系統。 在unix-like或Windows系統上為越來越大的字符串運行它可以產生完全不同的結果。 通常,在Windows下,您會看到運行時間增加得多。
Python實現。 默認情況下,我們討論CPython,但還有其他實現,如Jython或PyPy。 我們來看看PyPy。 使用上面答案中的源代碼:
CPython 2.7: python concat.py inplace_add_concatenation: 0.420897960663 str_join_concatenation: 0.061793088913 ratio: 6.81140833169 PyPy 1.9: pypy concat.py inplace_add_concatenation: 1.26573014259 str_join_concatenation: 0.0392870903015 ratio: 32.2174570038
雖然PyPy以其速度提升而聞名於CPython,但對於+=
版本來說速度較慢。 這是一個故意的設計,不包括默認版本的PyPy中的`+ ='優化。
處理表現的經驗法則:“永遠不要猜測,總是衡量。”
閱讀文檔也有助於:
6 CPython實現細節:如果s和t都是字符串,那么某些Python實現(如CPython)通常可以對s = s + t或s + = t形式的賦值執行就地優化。 如果適用,此優化使得二次運行時間的可能性大大降低。 此優化依賴於版本和實現。 對於性能敏感的代碼,最好使用str.join()方法,以確保跨版本和實現的一致的線性串聯性能。“”
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.