Python的51個“秘密”被曝光,GitHub獲2萬星
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)都如下所示:
> 一個精選的標(biāo)題
# 準(zhǔn)備代碼.
# 釋放魔法...
Output (Python version):
>>> 觸發(fā)語句
出乎意料的輸出結(jié)果
(可選): 對意外輸出結(jié)果的簡短描述。
說明:
簡要說明發(fā)生了什么以及為什么會發(fā)生。
如有必要,舉例說明
Output:
>>>觸發(fā)語句#一些讓魔法變得容易理解的例子
#一些正常的輸入
注意:所有的示例都在Python3.5.2版本的交互解釋器上測試過,如果不特別說明應(yīng)該適用于所有Python版本。
用法
我個人建議,最好依次閱讀下面的示例,并仔細閱讀設(shè)置例子最開始的代碼。
閱讀輸出結(jié)果
-
確認(rèn)結(jié)果是否如你所料.
-
確認(rèn)你是否知道這背后的原理
示例
微妙的字符串
1.
- >>> a = "some_string"
- >>> id(a)
- 140420665652016
- >>> id("some" + "_" + "string") # 注意兩個的id值是相同的.
- 140420665652016
2.
- >>> a = "wtf"
- >>> b = "wtf"
- >>> a is b
- True
- >>> a = "wtf!"
- >>> b = "wtf!"
- >>> a is b
- False
- >>> a, b = "wtf!", "wtf!"
- >>> a is b
- True
3.
- >>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
- True
- >>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
- False
說明:
這些行為是由于 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.
- some_dict = {}
- some_dict[5.5] = "Ruby"
- some_dict[5.0] = "JavaScript"
- some_dict[5] = "Python"
- Output:
- >>> some_dict[5.5]
- "Ruby"
- >>> some_dict[5.0]
- "Python"
- >>> some_dict[5]
- "Python"
- "Python" 消除了 "JavaScript" 的存在?
說明:
Python 字典通過檢查鍵值是否相等和比較哈希值來確定兩個鍵是否相同。
具有相同值的不可變對象在Python中始終具有相同的哈希值。
- >>> 5 == 5.0
- True
- >>> hash(5) == hash(5.0)
- True
注意: 具有不同值的對象也可能具有相同的哈希值(哈希沖突)。
當(dāng)執(zhí)行 some_dict[5] = "Python" 語句時, 因為Python將 5 和 5.0 識別為 some_dict 的同一個鍵, 所以已有值 "JavaScript" 就被 "Python" 覆蓋了。
到處返回!
- def some_func():
- try:
- return 'from_try'
- finally:
- return 'from_finally'
- Output:
- >>> some_func()
- 'from_finally'
說明:
當(dāng)在 "try...finally" 語句的 try 中執(zhí)行 return, break 或 continue 后, finally 子句依然會執(zhí)行。
函數(shù)的返回值由最后執(zhí)行的 return 語句決定。
由于 finally 子句一定會執(zhí)行, 所以 finally 子句中的 return 將始終是最后執(zhí)行的語句。
本質(zhì)上,我們都一樣
- class WTF:
- pass
- Output:
- >>> WTF() == WTF() # 兩個不同的對象應(yīng)該不相等
- False
- >>> WTF() is WTF() # 也不相同
- False
- >>> hash(WTF()) == hash(WTF()) # 哈希值也應(yīng)該不同
- True
- >>> id(WTF()) == id(WTF())
- True
說明:
當(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 呢? 讓我們看看這段代碼:
- class WTF(object):
- def __init__(self): print("I")
- def __del__(self): print("D")
- Output:
- >>> WTF() is WTF()
- I
- I
- D
- D
- False
- >>> id(WTF()) == id(WTF())
- I
- D
- I
- D
- True
正如你所看到的, 對象銷毀的順序是造成所有不同之處的原因。
為什么?
- some_string = "wtf"
- some_dict = {}
- for i, some_dict[i] in enumerate(some_string):
- pass
- Output:
- >>> some_dict # 創(chuàng)建了索引字典.
- {0: 'w', 1: 't', 2: 'f'}
說明:
Python 語法 中對 for 的定義是:
- for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
其中 exprlist 指分配目標(biāo). 這意味著對可迭代對象中的每一項都會執(zhí)行類似 {exprlist} = {next_value} 的操作。
一個有趣的例子說明了這一點:
- for i in range(4):
- print(i)
- i = 10
- Output:
- 0
- 1
- 2
- 3
你可曾覺得這個循環(huán)只會運行一次?
說明:
由于循環(huán)在Python中工作方式, 賦值語句 i = 10 并不會影響迭代循環(huán), 在每次迭代開始之前, 迭代器(這里指 range(4)) 生成的下一個元素就被解包并賦值給目標(biāo)列表的變量(這里指 i)了。
在每次迭代中, enumerate(some_string) 函數(shù)就生成一個新值 i (計數(shù)器增加) 并從 some_string 中獲取一個字符。
然后將字典 some_dict 鍵 i (剛剛分配的) 的值設(shè)為該字符。本例中循環(huán)的展開可以簡化為:
- >>> i, some_dict[i] = (0, 'w')
- >>> i, some_dict[i] = (1, 't')
- >>> i, some_dict[i] = (2, 'f')
- >>> some_dict
執(zhí)行時機差異
1.
- array = [1, 8, 15]
- g = (x for x in array if array.count(x) > 0)
- array = [2, 8, 22]
- Output:
- >>> print(list(g))
- [8]
2.
- array_1 = [1,2,3,4]
- g1 = (x for x in array_1)
- array_1 = [1,2,3,4,5]
- array_2 = [1,2,3,4]
- g2 = (x for x in array_2)
- array_2[:] = [1,2,3,4,5]
- Output:
- >>> print(list(g1))
- [1,2,3,4]
- >>> print(list(g2))
- [1,2,3,4,5]
說明:
在生成器表達式中, in 子句在聲明時執(zhí)行, 而條件子句則是在運行時執(zhí)行。
所以在運行前, array 已經(jīng)被重新賦值為 [2, 8, 22], 因此對于之前的 1, 8 和 15, 只有 count(8) 的結(jié)果是大于 0 的, 所以生成器只會生成 8。
第二部分中 g1 和 g2 的輸出差異則是由于變量 array_1 和 array_2 被重新賦值的方式導(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個案例了,更多案例大家可以在項目中查看。
項目作者:Satwik Kansal
英文版項目名稱:wtfpython
中文版作者:慕晨
中文項目名稱:wtfpython-cn
因為平臺規(guī)定,無法放鏈接。大家可以自行搜索。































