Python是否支持複製字符串呢?

圖片描述

連續幾篇文章都在寫 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》

https://www.zhihu.com/questio...

https://dwz.cn/4o0WXy8G

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


本文原創並首發於微信公衆號【Python貓】,後臺回覆“愛學習”,免費獲得20+本精選電子書。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章