Python是否支援複製字串呢?

豌豆花下貓發表於2018-11-28

連續幾篇文章都在寫 Python 字串,這出乎我的意料了。但是,有的問題,不寫不行,特別是那種靈機一動想到的問題,最後你發現,很多人根本不懂卻又誤以為自己懂了。那就繼續刨根問底,探究個明白吧。

在上一篇文章《你真的知道Python的字串怎麼用嗎?》裡,我突發奇想,將字串跟列表做了比較,然後發現字串竟然沒有複製的方法。當時沒有細想,只說要擱置疑問。過後,有好學的小夥伴在後臺留言,與我交流這個問題,給了我一些啟發。為了徹底弄懂它,我繼續查了不少資料,今天,就跟大家分享一下我發現的東西吧。

本文標題的問題分為兩部分:(1)Python 中是否支援複製字串?(2)如果不支援,為什麼不支援?

請讀者花幾分鐘想一下,想清楚後,把你的答案記住,然後再往下看。

讓我們做一個約定(自願遵守):如果看到最後,你推翻了現在的答案,建立了新的認知,這說明我寫的內容有用,那請你任意讚賞,或者將本文分享給其他使用 Python 的小夥伴。

1. 什麼是複製字串?

首先,必須要大家對“複製”這個概念達成共識。複製,也叫拷貝,英文單詞是 copy,具體意思是“將某事物通過某種方式製作成相同的一份或多份的行為”(釋義來自維基百科)。複製的結果是,出現了多份極其相似但卻相互獨立的事物(副本),舉例來說,你有一份文件 X,然後複製一份並重新命名為 Y,這兩者是相互獨立的,若你刪除其中一個,另一個不會一起被刪除。

這個詞用在 Python 裡,我們想表達的是同樣的意思,即複製行為會產生新的獨立物件,它與原始物件極其相似,但兩者的生命週期沒有直接的關聯關係。下面先用列表來舉例:

list1 = [1,2]
id(list1) 
>>> 1981119454856

list2 = list1.copy()
print(list1 == list2) 
>>> True
id(list2)
>>> 1981116983752
複製程式碼

上例中,列表 list2 是 list1 的副本,兩者字面量相等,但是記憶體地址(即 id )不相等,是兩個相互獨立的物件。如果字串能夠做到同樣的效果,那我們就說,字串可以被複制,否則,我們說字串不可以被複制。

2. 怎樣能複製字串?

有了上面的概念和示例,請先思考,你會用什麼方式複製字串呢?(暫停,思考3分鐘)

好了,先看看下面的幾種方法:

s0 = "Python貓"

s1 = s0
s2 = str(s0)
s3 = s0[:]
s4 = s0 + ''
s5 = '%s' % s0
s6 = s0 * 1
s7 = "".join(s0)
import copy
s8 = copy.copy(s0)
複製程式碼

你想到的複製方式是否在以上8種方式裡呢?那麼,如果把 s0 至 s8 的 id 列印出來,有哪些會跟 s0 不同呢?

答案是,它們的記憶體地址 id 完全相同,也就是說,一頓操作猛如虎,結果卻始終只有一份字串,根本沒有複製出新的字串!

Python貓 的老讀者看到這,會心一笑,這不就是因為字串的 Intern 機制嘛,短字串在記憶體中只會存在一份,在《Python中的“特權種族”是什麼?》這篇文章裡提到過的。

但請別開心得太早,你可以把 s0 改成一個超長的字串,例如:

s0 = "Python貓是來自喵星的客人,它喜歡地球和人類,正在學習Python,而且想借助Python變成人,它的微信公眾號也叫Python貓,歡迎你關注哦,喵喵喵喵~~~~~~~"

然後,再重複上面的操作。最終,你會發現,s0 到 s8 的 id 還是完全相同。

是不是吃驚了呢?新的 s0 明明已經超過 Intern 機制的長度了,為什麼不會產生新的字串呢?

首先,請你相信,超出 Intern 機制的字串可以存在多份,即你可以建立出值完全相同的多個字串物件,因為字串物件在記憶體中並不一定是唯一的:

s9 = "Python貓是來自喵星的客人,它喜歡地球和人類,正在學習Python,而且想借助Python變成人,它的微信公眾號也叫Python貓,歡迎你關注哦,喵喵喵喵~~~"

print(id(s0) == id(s9))
>>> False 
複製程式碼

上例表明,你可以建立出多個相同的字串物件,但是這種方法與前面列舉的8種不同,因為它是獨立於 s0 的操作,並不是一種複製操作。從理論上講,Python 完全可以提供一個方法,達到複製出新的副本的結果。現在的問題恰恰就是:為什麼允許存在多個相等的字串物件,但是卻無法通過複製的方式來建立呢?

3. 為什麼不允許複製字串?

我發現,不僅字串不允許複製,元祖也如此,事實上,還有 int 、float 也不支援複製。它們都是不可變物件,為什麼不可變物件就不支援複製操作呢?

在查資料的時候,我發現網上很多文章對於“不可變物件”的認識存在誤區,這些人不知道 Intern 機制的存在,誤以為字串物件在記憶體只能有唯一一個,進而誤以為不可變物件就是在記憶體中只有一份的物件。所以,這些文章很容易推斷出錯誤的結論:因為字串是不可變物件,所以字串不支援複製。

事實上,不可變物件跟複製操作之間,並沒有必然的強相關的關係。肯定是出於別的原因,設計者才給不可變物件加上這種限制,這個原因是什麼呢?

在知乎上,有敏銳的同學提出了我的疑問“Python中如何複製一個值或字串?”,可惜只有4個回答,而且都沒答到點上。Stackoverflow上恰好也有一個問題“How can I copy a Python string?”,同樣沒多少人注意到,只有5個回答,好在最高票答案提到了一個點,即這樣可以加快字典的查詢速度。

然而,他說的這個點並不靠譜。字典要求鍵值是可雜湊物件,可是計算字串的雜湊值是根據字面值計算,所以對多個相等的字串物件,其雜湊值其實是一樣的,對計算和查詢根本無影響。

w1 = "Python貓是來自喵星的客人,它喜歡地球和人類,正在學習Python,而且想借助Python變成人,它的微信公眾號也叫Python貓,歡迎你關注哦,喵喵喵喵~~~"
w2 = "Python貓是來自喵星的客人,它喜歡地球和人類,正在學習Python,而且想借助Python變成人,它的微信公眾號也叫Python貓,歡迎你關注哦,喵喵喵喵~~~"

print(w1 == w2) 
>>> True
print(id(w1) == id(w2)) 
>>> False 
print(hash(w1) == hash(w2)) 
>>> True
複製程式碼

繼續查資料,終於在《流暢的Python》找到了明確的解釋:

這些細節是 CPython 核心開發者走的捷徑和做的優化措施,對這門語言的使用者而言無需瞭解,而且那些細節對其他 Python 實現可能沒用,CPython 未來的版本可能也不會用。

這本《流暢的Python》是進階首選書目之一,我曾讀過部分章節,沒想到在一個不起眼的小節裡,作者 “驚訝地發現” 元祖的不可複製性,在此之前,他還自以為“對元祖無所不知”,哈哈哈。

雖然,我早猜測到原因是節省記憶體和提高速度,但看到這個明確的解釋,知道這只是CPython 直譯器的“善意的謊言”,而且在未來版本可能不會用,我感到特別意外。

它證實了我的猜測,同時,也提供了超預期的資訊:其它 Python 直譯器可能支援複製不可變物件,目前 CPython 算是一種妥協,在未來可能會恢復不可變物件的複製操作呢!

回到文章開頭的兩個問題,我們得到的答案是:Python 本身並不限制字串的複製操作,只是當前版本的 CPython 做了優化,才導致出現這種“善意的謊言”,它這麼做的原因為了對 Intern 機制做補充,設法使全部字串物件在記憶體都只有一份,以達到節省記憶體的效果。

CPython 是用 C 語言實現的 Python 直譯器,是官方的、使用最廣泛的直譯器。除了它,還有用 Java 實現的 Jython 直譯器、用 .NET 實現的 IronPython 直譯器、用 Python 實現的 PyPy 直譯器,等等。其它直譯器都是怎麼應對字串的複製操作的呢?唉,學無止境,本人才疏學淺沒有涉獵,還是先擱置疑問吧。

這裡,我就想提一個題外話,Python 最最最廣為人詬病的就是 GIL(全域性直譯器鎖),這導致它不支援真正意義的多執行緒,成為很多人指責 Python 慢的元凶。但是,這個問題是 CPython 直譯器帶來的,而像 Jython 直譯器就不存在這個問題。

好了,就此打住吧。你是否還記得在文章開頭時想到的答案呢?是否改變了最初的想法呢?歡迎關注公眾號 Python貓 ,來跟我交流,一起來學習 Python ,做個合格的 Pythonista

參考學習:

《流暢的Python》

www.zhihu.com/question/41…

dwz.cn/4o0WXy8G

最後是福利時刻:本公眾號(Python貓)由清華大學出版社贊助,將抽獎送出兩本新書《深入淺出Python機器學習》,截止時間到11月29日18:18,點選 這個連結,馬上參與吧。

相關文章