join()方法的神奇用處與Intern機制的軟肋

圖片描述
上篇文章《Python是否支持複製字符串呢?》剛發出一會,@發條橙 同窗就在後臺留言,指出了一處錯誤。我一驚,立刻去驗證,居然真的錯了,並且在徹底沒意料到的地方!我開始覺得只是疏漏,一細想,發現不簡單,遇到了百思不得其解的問題了。因此,這篇文章還得再聊聊字符串。python

照例先總結下本文內容:(1)join() 方法除了在拼接字符串時速度較快,它仍是目前看來最通用有效的複製字符串的方法 (2)Intern 機制(字符串滯留)並不是萬能的,本文探索一下它的軟肋有哪些微信

1. join()方法不止是拼接

我先把那個問題化簡一下吧:ide

ss0 = 'hi'
ss1 = 'h' + 'i'
ss2 = ''.join(ss0)

print(ss0 == ss1 == ss2) >>> True
print(id(ss0) == id(ss1)) >>> True
print(id(ss0) == id(ss2)) >>> False

上面代碼中,奇怪的地方就在於 ss2 居然是一個獨立的對象!按照最初想固然的認知,我認定它會被 Intern 機制處理掉,因此是不會佔用獨立內存的。上篇文章快寫完的時候,我忽然想到 join 方法,因此沒作驗證就臨時加進去,致使了意外的發生。性能

按照以前在「特權種族」那篇文章的總結,我對字符串 Intern 機制有這樣的認識:學習

Python中,字符串使用Intern機制實現內存地址共用,長度不超過20,且僅包括下劃線、數字、字母的字符串纔會被intern;涉及字符串拼接時,編譯期優化結果會與運行期計算結果不一樣。

爲何 join 方法拼接字符串時,能夠不受 Intern 機制做用呢?優化

回看那篇文章,發現可能存在編譯期與運行期的差異!網站

# 編譯對字符串拼接的影響
s1 = "hell"
s2 = "hello"
"hell" + "o" is s2 
>>>True
s1 + "o" is s2 
>>>False
# "hell" + "o"在編譯時變成了"hello",
# 而s1+"o"由於s1是一個變量,在運行時才拼接,因此沒有被intern

實驗一下,看看:ui

# 代碼加上
ss3 = ''.join('hi')
print(id(ss0) == id(ss3)) >>> False

ss3 仍然是獨立對象,難道這種寫法仍是在運行期時拼接?那怎麼判斷某種寫法在編譯期仍是在運行期起做用呢?繼續實驗:spa

s0 = "Python貓"
import copy
s1 = copy.copy(s0)
s2 = copy.copy("Python貓")

print(id(s0) == id(s1))
>>> True
print(id(s0) == id(s2))
>>> False

看來,不能經過是否顯性傳值來判斷。3d

那就只能從 join 方法的實現原理入手查看了。經某交流羣的小夥伴提醒,能夠去 Python Tutor 網站,看看可視化執行過程。可是,很遺憾,也沒看出什麼底層機制。

我找了分析 CPython 源碼的資料(含上期薦書欄目的《Python源碼剖析》)來學習,可是,這些資料只比較 join() 方法與 + 號拼接法在原理與使用內存上的差別,並沒說起爲什麼 Intern 機制對前者會失效,而對後者倒是生效的。

現象已經產生,我只能暫時解釋說,join 方法會不受 Intern 機制控制,它有獨享內存的「特權」。

那就是說,其實有複製字符串的方法!上篇《Python是否支持複製字符串呢?》因爲沒有發現這點,最後得出了錯誤的結論!

因爲這個特例,我要修改上篇文章的結論了:Python 自己並不限制字符串的複製操做,CPython 解釋器出於優化性能的考慮,加入了一些小把戲,試圖使字符串對象在內存中只有一份,儘管如此,仍存在有效複製字符串的方法,那就是 join() 方法。

2. Intern 機制失效的狀況

join() 方法的神奇用處使我不得不改變對 Intern 機制的認識,本小節就帶你們從新學習一下 Intern 機制吧。

所謂 Intern 機制,即字符串滯留(string interning),它經過維護一個字符串常量池(string intern pool),從而試圖只保存惟一的字符串對象,達到既高效又節省內存地處理字符串的目的。在建立一個新的字符串對象後,Python 先比較常量池中是否有相同的對象(interned),有的話則將指針指向已有對象,並減小新對象的指針,新對象因爲沒有引用計數,就會被垃圾回收機制回收掉,釋放出內存。

Intern 機制不會減小新對象的建立與銷燬,但最終會節省出內存。這種機制還有另外一個好處,即被 Interned 的相同字符串做比較時,幾乎不花時間。實驗數據以下(資料來源:http://t.cn/ELu9n7R):

Intern 機制的大體原理很好理解,然而影響結果的還有 CPython 解釋器的其它編譯及運行機制,字符串對象受到這些機制的共同影響。實際上,只有那些「看起來像」 Python 標識符的字符串纔會被處理。源代碼StringObject.h的註釋中寫道:

/ … … This is generally restricted to strings that 「looklike」 Python identifiers, although the intern() builtin can be used to force interning of any string … … /

這些機制的相互做用,不經意間帶來了很多混亂的現象:

# 長度超過20,不被intern VS 被intern
'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
>>> False
'aaaaaaaaaaaaaaaaaaaaa' is 'aaaaaaaaaaaaaaaaaaaaa'
>>> True

# 長度不超過20,不被intern VS 被intern
s = 'a'
s * 5 is 'aaaaa'
>>> False
'a' * 5 is 'aaaaa'
>>> True


# join方法,不被intern VS 被intern
''.join('hi') is 'hi'
>>> False
''.join('h') is 'h'
>>> True

# 特殊符號,不被intern VS 被"intern"
'python!' is 'python!'
>>> False
a, b = 'python!', 'python!'
a is b
>>> True

這些現象固然都能被合理解釋,然而因爲不一樣機制的混合做用,就很容易形成誤會。好比第一個例子,不少介紹 Intern 機制的文章在比較出 'a' * 21 的id有變化後,就認爲 Intern 機制只對長度不超過20的字符串生效,但是,當看到長度超過20的字符串的id還相等時,這個結論就變錯誤了。當加入常量合併(Constant folding) 的機制後,長度不超過20的字符串會被合併的現象才獲得解釋。但是,在 CPython 的源碼中,只有長度不超過1字節的字符串纔會被 intern ,爲什麼長度超標的狀況也出現了呢? 再加入 CPython 的編譯優化機制,才能解釋。

因此,看似被 intern 的兩個字符串,實際可能不是 Intern 機制的結果,而是其它機制的結果。一樣地,看似不能被 intern 的兩個字符串,實際可能被其它機制以相似方式處理了。

如此種種,便提升了理解 Intern 機制的難度。

就我在上篇文章中所關心的「複製字符串」話題而言,只有當 Intern 機制與其它這些機制通通失效時,才能作到複製字符串。目前看來,join 方法最具通用性。

3. 學習的方法論

總而言之,由於從新學習 join 方法的神奇用處與 Intern 機制的例外狀況,我得以修正上篇文章的錯誤。在此過程當中,我獲得了新的知識,以及思考學習的樂趣。

《超人》電影中有一句著名的臺詞,在今年上映的《頭號玩家》中也出現了:

有的人從《戰爭與和平》裏看到的只是一個普通的冒險故事,

有的人則能經過閱讀口香糖包裝紙上的成分表來解開宇宙的奧祕。

我讀到的是一種敏銳思辨的思想、孜孜求索的態度和以小窺大的方法。做爲一個低天賦的人,受此鼓舞,我會繼續追問那些看似沒意義的問題(「如何刪除字符串」、「如何複製字符串」...),一點一點地學習 Python ,以個人方式理解它。同時,但願能給個人讀者們帶來一些收穫。

PS.很多人在期待 「Python貓」 系列,別急哈,讓那隻貓再睡幾天,等它醒來,我替你們催它!

字符串系列文章:

詳解Python拼接字符串的七種方式

你真的知道Python的字符串是什麼嗎?

你真的知道Python的字符串怎麼用嗎?

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

Python貓系列:

有了Python,我能叫出全部貓的名字

Python對象的身份迷思:從全體公民到萬物皆數

-----------------

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

相關文章
相關標籤/搜索