文 | 網(wǎng)絡(luò) 編輯 | Earlgrey 推薦 | 編程派公眾號(ID:codingpy) Python,是一個設(shè)計優(yōu)美的解釋型高級語言,它提供了很多能讓程序員感到舒適的功能特性。 但有的時候,Python一些特性導(dǎo)致的輸出結(jié)果,對于初學(xué)者就很難理解了。 一個解析51項堪稱是'秘密'的Python特性項目,在GitHub上徹底火了。 英文原版已經(jīng)拿到了近15000星,中文翻譯版也獲得了7500+星。 項目中的部分內(nèi)容,也許你聽說過,但依然可能會透露一些你所不知道的Python有趣特性。 我覺得這是學(xué)習(xí)編程語言內(nèi)部原理的好機會,而且我相信你也會從中獲得樂趣! 如果你是一位經(jīng)驗比較豐富的Python程序員,你可以試試能否一次就找到正確答案。 也許你對其中的一些例子比較熟悉,那這些案例能喚起你當(dāng)年踩坑時的甜蜜回憶。 這個項目的中文版全文大約2萬字,干貨多的快要溢出來了,大家可以先看一下目錄。 示例結(jié)構(gòu) 所有示例的結(jié)構(gòu)都如下所示:
我個人建議,最好依次閱讀下面的示例,并仔細閱讀設(shè)置例子最開始的代碼。 閱讀輸出結(jié)果 示例 微妙的字符串 1.
2.
3.
說明: 這些行為是由于 Cpython 在編譯優(yōu)化時,某些情況下會嘗試使用已經(jīng)存在的不可變對象,而不是每次都創(chuàng)建一個新對象。(這種行為被稱作字符串的駐留[string interning]) 發(fā)生駐留之后,許多變量可能指向內(nèi)存中的相同字符串對象。(從而節(jié)省內(nèi)存) 在上面的代碼中,字符串是隱式駐留的。何時發(fā)生隱式駐留則取決于具體的實現(xiàn)。這里有一些方法可以用來猜測字符串是否會被駐留: 所有長度為 0 和長度為 1 的字符串都被駐留。 字符串在編譯時被實現(xiàn)。('wtf' 將被駐留, 但是 ''.join(['w', 't', 'f'] 將不會被駐留) 字符串中只包含字母,數(shù)字或下劃線時將會駐留。所以 'wtf!' 由于包含!而未被駐留??梢栽谶@里找CPython對此規(guī)則的實現(xiàn)。 當(dāng)在同一行將 a 和 b 的值設(shè)置為 'wtf!' 的時候, Python 解釋器會創(chuàng)建一個新對象, 然后同時引用第二個變量。 如果你在不同的行上進行賦值操作, 它就不會'知道'已經(jīng)有一個 wtf!對象 (因為 'wtf!' 不是按照上面提到的方式被隱式駐留的)。 它是一種編譯器優(yōu)化,特別適用于交互式環(huán)境。 常量折疊(constant folding) 是 Python 中的一種窺孔優(yōu)化(peephole optimization) 技術(shù)。 這意味著在編譯時表達式 'a'*20 會被替換為 'aaaaaaaaaaaaaaaaaaaa' 以減少運行時的時鐘周期。 只有長度小于 20 的字符串才會發(fā)生常量折疊。(為啥? 想象一下由于表達式'a'10*10 而生成的 .pyc 文件的大小)相關(guān)的源碼: https://github.com/python/cpython/blob/3.6/Python/peephole.c#L288 是時候來點蛋糕了! 1.
說明: Python 字典通過檢查鍵值是否相等和比較哈希值來確定兩個鍵是否相同。 具有相同值的不可變對象在Python中始終具有相同的哈希值。
注意: 具有不同值的對象也可能具有相同的哈希值(哈希沖突)。 當(dāng)執(zhí)行 somedict5 = 'Python' 語句時, 因為Python將 5 和 5.0 識別為 somedict 的同一個鍵, 所以已有值 'JavaScript' 就被 'Python' 覆蓋了。 到處返回!
說明: 當(dāng)在 'try...finally' 語句的 try 中執(zhí)行 return, break 或 continue 后, finally 子句依然會執(zhí)行。 函數(shù)的返回值由最后執(zhí)行的 return 語句決定。 由于 finally 子句一定會執(zhí)行, 所以 finally 子句中的 return 將始終是最后執(zhí)行的語句。 本質(zhì)上,我們都一樣
說明: 當(dāng)調(diào)用 id 函數(shù)時, Python 創(chuàng)建了一個 WTF 類的對象并傳給 id 函數(shù)。 然后 id 函數(shù)獲取其id值 (也就是內(nèi)存地址), 然后丟棄該對象. 該對象就被銷毀了。 當(dāng)我們連續(xù)兩次進行這個操作時, Python會將相同的內(nèi)存地址分配給第二個對象。因為 (在CPython中) id 函數(shù)使用對象的內(nèi)存地址作為對象的id值, 所以兩個對象的id值是相同的。 綜上, 對象的id值僅僅在對象的生命周期內(nèi)唯一。在對象被銷毀之后, 或被創(chuàng)建之前, 其他對象可以具有相同的id值。 那為什么 is 操作的結(jié)果為 False 呢? 讓我們看看這段代碼:
正如你所看到的, 對象銷毀的順序是造成所有不同之處的原因。 為什么?
說明: Python 語法 中對 for 的定義是:
其中 exprlist 指分配目標(biāo). 這意味著對可迭代對象中的每一項都會執(zhí)行類似 {exprlist} = {next_value} 的操作。 一個有趣的例子說明了這一點:
你可曾覺得這個循環(huán)只會運行一次? 說明: 由于循環(huán)在Python中工作方式, 賦值語句 i = 10 并不會影響迭代循環(huán), 在每次迭代開始之前, 迭代器(這里指 range(4)) 生成的下一個元素就被解包并賦值給目標(biāo)列表的變量(這里指 i)了。 在每次迭代中, enumerate(somestring) 函數(shù)就生成一個新值 i (計數(shù)器增加) 并從 somestring 中獲取一個字符。 然后將字典 some_dict 鍵 i (剛剛分配的) 的值設(shè)為該字符。本例中循環(huán)的展開可以簡化為:
執(zhí)行時機差異 1.
2.
說明: 在生成器表達式中, in 子句在聲明時執(zhí)行, 而條件子句則是在運行時執(zhí)行。 所以在運行前, array 已經(jīng)被重新賦值為 [2, 8, 22], 因此對于之前的 1, 8 和 15, 只有 count(8) 的結(jié)果是大于 0 的, 所以生成器只會生成 8。 第二部分中 g1 和 g2 的輸出差異則是由于變量 array1 和 array2 被重新賦值的方式導(dǎo)致的。 在第一種情況下, array_1 被綁定到新對象 [1,2,3,4,5], 因為 in 子句是在聲明時被執(zhí)行的,所以它仍然引用舊對象 1,2,3,4。 在第二種情況下, 對 array_2 的切片賦值將相同的舊對象 [1,2,3,4] 原地更新為 [1,2,3,4,5]。 因此 g2 和 array_2 仍然引用同一個對象(這個對象現(xiàn)在已經(jīng)更新為 [1,2,3,4,5])。 本文內(nèi)容來自中文版項目,項目全文2萬多字,以及海量代碼。 因為篇幅原因,小七就只為大家展示這6個案例了,更多案例大家可以在項目中查看。 |
|