
Python中有效的字符串合并方法
在Python編程語言中,構造一些較長的字符串事常常會產生一些運行很慢的代碼。本文我將研究不同字符串合并方法的計算性能。
在Python中,字符串(string)對象是不可變的(每次關聯一個新的字符串變量都會在內存中創建一個新的對象)(譯注:類同于Java,.NET等現代語言,他們都會在其VM中保留一個字符串池,里面保存所有產生的目標字符串值或臨時字符串值)。這方面它與perl、VB等語言中的字符串變量可以任意修改有所不同。如果使用一些比較顯而易見的方法(比如:每次都是在新產生的字符串末尾添加一個新短字符串片段)從一些短字符串片段構造長字符串在Python中可能會不是很有效率。每次你的在字符串末尾添加內容,Python解釋器都會創建一個新的對象并且復制新產生的對象和原來的對象到解釋器中(譯注:應該是復制到Python解釋器的字符串常量池中)。隨著處理的字符串的增多,這樣的處理過程將會越來越慢。
其他一些其他的方法呢?他們是否有效并且與原始方法相比它們性能方面如何?我決定試試一些其他的構造長字符串的方法,并看看它們在效率上都有啥不同。
為了比較,我需要一個測試程序來調用大量的字符串片段構造長字符串。它不應該有太多的額外計算,好讓我們測試的性能僅僅依賴于字符串操作的性能。
我的測試用例是合并一些從0到某個大整數的數字。這樣我們也可以很容易的改變需要產生字符串的大小(譯注:改變那個大整數)。比如前20個整數產生如下的字符串:
0123456789010111213141516171819
盡管這個特別的測試問題不會有任何的現實應用,但我想,因為它很容易編程并且在概念和計算上都簡單,那么它能是一個很好的測試用例。這些字符串片段在值和長度上都不同,這也可以防止解釋器或硬件對依賴于重復字節的優化(譯注:比如對重復相同的字符串進行壓縮等處理)。我不認為Python解釋器真的這樣做了,但是作為測試的一個好原則就是不能受這種優化情況的影響。
六個方法
下面是我測試的一些方法,每小段Python代碼都返回相同的字符串。
方法一:樸素的添加(Method 1: Naive appending)
def method1():
out_str = ''
for num in xrange(loop_count):
out_str += `num`
return out_str
對于我來說,這是解決該問題的最顯而易見的方法。使用字符串連接操作(+=)添加每個字符串片段到字符串中。loop_count告訴我們要添加的字符串片段數。第四行中的數字num兩邊的重音符(``)會把整數轉換為相對于的字符串。你可以使用str()方法完成一樣的功能,但是,比較起來它可能稍慢些,因此我所有的方法中都是使用重音符(``)。如我說言,盡管很淺顯,但是這個方法根本不是很有效(譯注:maybe應該加個”率”子)。你可以再下面的測試中看到它每秒僅僅能合并3770個字符串片段。如果你需要合并很多的字符串片段,那么這可能不是很好的解決方法。
方法二:MutableString 類(Method 2: MutableString class)
def method2():
from UserString import MutableString
out_str = MutableString()
for num in xrange(loop_count):
out_str += `num`
return out_str
Python類庫中包括一個MutableString類。根據其文檔描述它主要用于教學目的(譯注: "mutable string objects Python strings are immutable objects. This has the advantage, that strings may be used as dictionary keys. If this property isn't needed and you insist on changing string values in place instead, you may cheat and use MutableString. But the purpose of this class is an educational one: to prevent people from inventing their own mutable string class derived from UserString and than forget thereby to remove (override) the __hash__ method inherited from UserString. This would lead to errors that would be very hard to track down. A faster and better solution is to rewrite your program using lists.")。你可以能會以為在一個可變字符串上添加操作不會從分配或者拷貝字符串(譯注:本來該類應該很像Java的StringBuilder的)。但是在測試中該方法比方法1還差。通過查看UserString.py的源代碼我發現字符串在MutableString中的存儲就是string,MutableString甚至都沒重寫__add__方法。所以使用該類對象合并字符串不會比一般的不可變字符串更快,實際上由于解釋MutableString方法需要一些額外的開銷會使得該方法更慢。
方法三:字符數組(Method 3: Character arrrays)
def method3():
from array import array
char_array = array('c')
for num in xrange(loop_count):
char_array.fromstring(`num`)
return char_array.tostring()
我幾乎都沒有嘗試這種方法,但是郵件列表中有人提到了,所以我決定試試。該方法的思想就是用字符數組存儲字符串。Python中的數組是可變的,所以它可以被原地改變(譯注:也就是在該對象的那塊內存上進行改變,而不需要通過復制到其他的空間上實現)而不需要拷貝現存的數組內容。這里我們對改變現存的數組元素沒有興趣。我們只是在數組末尾添加一些新的數組元素。fromstring()方法一個字符一個字符的添加字符串字符到字符數組對象中。
方法四:構造一個字符串列表,然后join它(Method 4: Build a list of strings, then join it)
def method4():
str_list = []
for num in xrange(loop_count):
str_list.append(`num`)
return ''.join(str_list)
這是一種通常被推薦的方法,因為它的字符串合并方法很Python。首先構造一個包含所有需要合并的字符串列表,然后使用一個字符串的join操作構造包含所有列表元素的字符串。
這人有點好玩,看看python習語的最后一行-我們在確定的空字符串上調用join方法。不是所有的語言都會讓你在一個字面上的字符串調用方法(譯注:這里的意識是’’是空字符串)。如果你覺得這兒有點不爽,你可以寫成如下形式: string.join(str_list, '')。
方法五:寫到一個偽文件中去(Method 5: Write to a pseudo file)
def method5():
from cStringIO import StringIO
file_str = StringIO()
for num in xrange(loop_count):
file_str.write(`num`)
return file_str.getvalue()
cStringIO模塊提供的StringIO類可以像文件一樣工作,但是它存儲為一個字符串。很明顯,添加內容到文件中是很容易的—你可以簡單的寫入到文件末尾,對StringIO類對象的操作也是一樣。還有一個相似的模塊叫StringIO, 不過它是以Python實現的,而cStringIO是用C實現的,所以cStringIO速度上會更快。使用cStringIO對象,我么可以構造一個每次寫入一次內容的字符串,然后通過調用getvalue()方法收集其中的所有內容。
有意思的是,同python類似,在java中字符串也是不可變的對象。Java中有個類叫StringBuffer,它比python中的StringIO和數組方法都更加強大,因為它不僅支持添加字符串還支持插入和刪除子字符串操作。
方法六:列表推導 (Method 6: List comprehensions)
def method6():
return ''.join([`num` for num in xrange(loop_count)])
方法的代碼是最短的。并且令人驚喜的是他也是最快的。它及其緊湊并且也非常好理解。使用列表推導創建一個列表并使用join方法合并它們。還如比這個簡單的嗎?實際上這是方法4的簡略版,當然,它也需要消耗差不多的空間。它更快是因為不需要在循環的每次都調用list.append()方法。
測試結果
我想要查看合并字符串時所花費的時間和計算時Python解釋器的內存使用情況。盡管內存很便宜(譯注:這里應該是內存開銷不是非常大的意思),但是依然有很多原因使其成為一個重要的因素。首先,Python程序常常會運行在資源受限的系統上。例如,在一個共享虛擬主機的環境下,機器可能對每個進程設置了一定大小的內存使用。系統內核往往會殺死內存分配超過一定額度的進程。這種情況對于一些CGI腳本(譯注: Computer Graphics Interface),長時運行的服務器程序來說將是不好的現象。所以在這種情況下,保存內存使用不超過預期是很重要的。另一個原因是當我們處理大量的字符串的時候,解釋器的內存分配將會變得非常大可能會導致虛擬內存的訪問(譯注:paging是操作系統中的一個概念,表示對硬盤頁面的訪問)。這種情況下的性能將會直線下降。如果你發現了時間上最快的算法當然無所謂了---如果它使用了過多的內存將會允許得和狗(譯注:應該是像蝸牛吧,J)一樣慢。如果用的算法使用更少的內存,那么就會介紹paging的機會,我們也將會有更多的可預測性能。
我使用自己的Python過程分別測試每種方法。
我在一臺按照了FreeBSD 4.9 的433MHz PII Celeron機器上使用Python 2.2.1運行這些測試程序。
結果:兩萬個整數(Results: Twenty thousand integers)
第一個測試將兩萬個整數合并成一個86kb大小的字符串。
結果:五十萬個整數(Results: Five hundred thousand integers)
接下來我測試了將五十萬個整數合并成一個2,821kb大小的字符串。這個測試更嚴厲,我們開始想看看Python解釋器進程的使用資源大小隨著用于計算的數據結構變化情況。
這個測試中我沒有運行方法1和方法2。他們的每次append操作都需要拷貝整個原串,因此他們的性能將是O(n^2)的(譯注:這個地方不是很理解,可能說的是包括在常量池中尋找字符串的時間)。使用他們再合并數十萬個字符串時可能要花數分鐘。
從圖中可以看書,與前面一個測試相比較,方法3,4,6在規模增大時每秒合并的字符串數目在減少。這個不奇怪(由于整數值的增大,相對于的字符串表示也在增大,大概前一個測試中的5個相當于該測試的4個吧)。在第一個測試中,方法3比前兩個方法塊了近10倍,但是它性能在更大規模數據上的測試并沒有相應的提升。其實它比之前還慢了60%,但相較于其他方法它使用了更少的空間。很明顯,Python在有效的存儲數組和臨時字符串的垃圾回收上做了一些工作。
方法4在性能上比樸素的添加提高了很多,在兩萬個數據的測試中提高了近20倍在五十萬的數據上也有很好的提高。方法6始終是最好的,但是方法5也有很好的性能,并且直追方法6。我們猜測,當測試更大規模數據的時候,方法5會超過方法6。
我們也要注意到空間開銷的大小。方法6的解釋器使用了22,844kB的內存,8倍于其實際的大小,反而方法3和方法5僅僅使用了其一般的內存。
總結
我在實際編程中常常使用方法7,它很快并且也很好理解。它僅需要你寫一個返回需要添加字符串的表達式。有的時候這可能不是很方便,比如:有多個不同的字符串快需要用于合并時,這種情況下,你可能需要使用方法4和方法5。
方法4在可行上更好。你可以使用在添加的字符串列表上切片,或者插入,刪除和修改操作。它在添加操作上的性能也是很好的。
方法五在效率上更好。它使用更少的內存(遠少于方法4和6),并且在處理大量數據(大概多于700,000時)時比列表推導的更快。如果你需要添加非常多的字符串,cStringIO是一個很好的方向。
測試技術
計算每個方法的執行時間相對來說還比較容易。我使用Python類庫中的timing模塊計算花費的時間。我沒有試圖去該除了運行于該機上的其他程序,單獨計算所運行的Python程序的CPU時間開銷,但是除了Python程序,機器是空閑的,所以我不認為此處計算出的時間與CPU的運行時間有什么不一樣的。
計算內存開銷就有點困難了,因為Python本身沒有提供方法用于監控其所分配對象的空間占用大小(譯注:這點上JVM做的很好,它的tools.jar包里面有很多性能監控的工具),所以我使用了UNIX的’PS’命令去監控它。因為占用空間會隨著程序的運行而改變,而我想計算其最大的分配空間。為了得到結果,我在計算完成的時候運行’ps’命令。ps_stat()的調用插在合并方法return語句前,因此可以在垃圾回收啟動前計算其程序占用空間大小。我稍稍的改變了一點你在上面看到的代碼---ps_stat()運行的計算結果用一個字符串變量存儲。我執行的ps_stat()方法分離ps命令返回的各個域項并選擇內存使用大小項所對應的值。這里是使用15,可能不同版本的ps程序需要不同的值。
我使用的全部代碼在這里。
from cStringIO import StringIO
import timing, commands, os
from sys import argv
# .....
# method definitions go here
# .....
def ps_stat():
global process_size
ps = commands.getoutput('ps -up ' + `pid`)
process_size = ps.split()[15]
def call_method(num):
global process_size
timing.start()
z = eval('method' + str(num))()
timing.finish()
print "method", num
print "time", float(timing.micro()) / 1000000
print "output size ", len(z) / 1024, "kb"
print "process size", process_size, "kb"
loop_count = 500000
pid = os.getpid()
call_method(argv[1])
備注1:翻譯自文章Efficient String Concatenation in Python
備注2:在文章題目的翻譯上,我糾結于“Python中有效的字符串拼接方法”和“Python中有效的字符串合并方法”很久,最后還是覺得第一句中太口語化了,并且可能也僅僅是我自己喜歡拼接這個詞,所以還是選擇了“合并”。
備注3:其實這些方法中我用到較多的還是方法3和方法6,其他的還真不太了解。另外,文章中的那兩個圖,我沒有通過自己的實驗數據結果來畫,主要還是我不太會畫。Python還不是很熟悉,那些繪圖模塊更不熟悉了,這個還有加以學習。
備注4:文章后面還有兩段內容,不過與本文主題沒有什么關系,就沒有翻譯了。這篇文章也算是正了八經的翻譯的一篇吧。以前翻譯的都是只有幾段文字的短文。曾試圖翻譯過wikipedia的article,但最終都沒成文。讀書的時候,我其實是很不喜歡,也不是非常欣賞那種通過翻譯文章來理解文章的做法,但是現在我發現我錯了,理解固然主要,但如果能翻譯那就是更高層次的理解;很多時候,我覺得對原文理解了,但是沒法用自己的母語去描述它,這說明我還是沒有很好的理解。就像對某個概念的理解,我做不到能書寫或者講授,那也說明我沒有理解透徹。
最好,這篇文章也翻譯的匆忙,肯定不如人意的地方大大多于有點價值的地方。還忘大家不要吝嗇批評指正
數據分析咨詢請掃描二維碼
若不方便掃碼,搜微信號:CDAshujufenxi
CDA數據分析師證書考試體系(更新于2025年05月22日)
2025-05-26解碼數據基因:從數字敏感度到邏輯思維 每當看到超市貨架上商品的排列變化,你是否會聯想到背后的銷售數據波動?三年前在零售行 ...
2025-05-23在本文中,我們將探討 AI 為何能夠加速數據分析、如何在每個步驟中實現數據分析自動化以及使用哪些工具。 數據分析中的AI是什么 ...
2025-05-20當數據遇見人生:我的第一個分析項目 記得三年前接手第一個數據分析項目時,我面對Excel里密密麻麻的銷售數據手足無措。那些跳動 ...
2025-05-20在數字化運營的時代,企業每天都在產生海量數據:用戶點擊行為、商品銷售記錄、廣告投放反饋…… 這些數據就像散落的拼圖,而相 ...
2025-05-19在當今數字化營銷時代,小紅書作為國內領先的社交電商平臺,其銷售數據蘊含著巨大的商業價值。通過對小紅書銷售數據的深入分析, ...
2025-05-16Excel作為最常用的數據分析工具,有沒有什么工具可以幫助我們快速地使用excel表格,只要輕松幾步甚至輸入幾項指令就能搞定呢? ...
2025-05-15數據,如同無形的燃料,驅動著現代社會的運轉。從全球互聯網用戶每天產生的2.5億TB數據,到制造業的傳感器、金融交易 ...
2025-05-15大數據是什么_數據分析師培訓 其實,現在的大數據指的并不僅僅是海量數據,更準確而言是對大數據分析的方法。傳統的數 ...
2025-05-14CDA持證人簡介: 萬木,CDA L1持證人,某電商中廠BI工程師 ,5年數據經驗1年BI內訓師,高級數據分析師,擁有豐富的行業經驗。 ...
2025-05-13CDA持證人簡介: 王明月 ,CDA 數據分析師二級持證人,2年數據產品工作經驗,管理學博士在讀。 學習入口:https://edu.cda.cn/g ...
2025-05-12CDA持證人簡介: 楊貞璽 ,CDA一級持證人,鄭州大學情報學碩士研究生,某上市公司數據分析師。 學習入口:https://edu.cda.cn/g ...
2025-05-09CDA持證人簡介 程靖 CDA會員大咖,暢銷書《小白學產品》作者,13年頂級互聯網公司產品經理相關經驗,曾在百度、美團、阿里等 ...
2025-05-07相信很多做數據分析的小伙伴,都接到過一些高階的數據分析需求,實現的過程需要用到一些數據獲取,數據清洗轉換,建模方法等,這 ...
2025-05-06以下的文章內容來源于劉靜老師的專欄,如果您想閱讀專欄《10大業務分析模型突破業務瓶頸》,點擊下方鏈接 https://edu.cda.cn/g ...
2025-04-30CDA持證人簡介: 邱立峰 CDA 數據分析師二級持證人,數字化轉型專家,數據治理專家,高級數據分析師,擁有豐富的行業經驗。 ...
2025-04-29CDA持證人簡介: 程靖 CDA會員大咖,暢銷書《小白學產品》作者,13年頂級互聯網公司產品經理相關經驗,曾在百度,美團,阿里等 ...
2025-04-28CDA持證人簡介: 居瑜 ,CDA一級持證人國企財務經理,13年財務管理運營經驗,在數據分析就業和實踐經驗方面有著豐富的積累和經 ...
2025-04-27數據分析在當今信息時代發揮著重要作用。單因素方差分析(One-Way ANOVA)是一種關鍵的統計方法,用于比較三個或更多獨立樣本組 ...
2025-04-25CDA持證人簡介: 居瑜 ,CDA一級持證人國企財務經理,13年財務管理運營經驗,在數據分析就業和實踐經驗方面有著豐富的積累和經 ...
2025-04-25