Python 是一個設計優美的解釋型高級語言,它提供了很多能讓程序員感到舒適的功能特性。但有的時候,Python 的一些輸出結果對於初學者來說似乎並不是那麼一目瞭然。
這個有趣的項目意在收集 Python 中那些難以理解和反人類直覺的例子以及鮮為人知的功能特性,並嘗試討論這些現象背後真正的原理!
雖然下面的有些例子並不一定會讓你覺得 WTFs,但它們依然有可能會告訴你一些你所不知道的 Python 有趣特性。我覺得這是一種學習編程語言內部原理的好辦法,而且我相信你也會從中獲得樂趣!
如果您是一位經驗比較豐富的 Python 程序員,你可以嘗試挑戰看是否能一次就找到例子的正確答案。你可能對其中的一些例子已經比較熟悉了,那這也許能喚起你當年踩這些坑時的甜蜜回憶~
如果你不是第一次讀了, 你可以在這裡獲取變動內容.
更新說明:https://github.com/satwikkansal/wtfpython/releases/
項目地址:https://github.com/leisurelicht/wtfpython-cn
英文版:https://github.com/satwikkansal/wtfpython
pdf 版:http://www.satwikkansal.xyz/wtfpython-pdf/
那麼,讓我們開始吧...
Structure of the Examples/示例結構
所有示例的結構都如下所示:
> 一個精選的標題 *
標題末尾的星號表示該示例在第一版中不存在,是最近添加的。
# 準備代碼.# 釋放魔法...
Output (Python version):
(可選):對意外輸出結果的簡短描述。
說明:
簡要說明發生了什麼以及為什麼會發生。
如有必要, 舉例說明
Output:
注意: 所有的示例都在 Python 3.5.2 版本的交互解釋器上測試過, 如果不特別說明應該適用於所有 Python 版本.
Usage/用法
我個人建議,最好依次閱讀下面的示例,並對每個示例:
仔細閱讀設置例子最開始的代碼。如果您是一位經驗豐富的 Python 程序員,那麼大多數時候您都能成功預期到後面的結果。
閱讀輸出結果,
確認結果是否如你所料。
確認你是否知道這背後的原理。
如果不知道, 深呼吸然後閱讀說明 (如果你還是看不明白, 別沉默!可以在這 (https://github.com/satwikkansal/wtfPython) 提個 issue)。
如果知道,給自己點獎勵,然後去看下一個例子。
PS: 你也可以在命令行閱讀 WTFpython. 我們有 pypi 包 和 npm 包 (支持代碼高亮)。(譯: 這兩個都是英文版的)
安裝 npm 包 wtfpython(https://www.npmjs.com/package/wtfpython)
$ npm install -g wtfpython
或者, 安裝 pypi 包 wtfpython(https://pypi.python.org/pypi/wtfpython)
$ pip install wtfpython -U
現在, 在命令行中運行 wtfpython, 你就可以開始瀏覽了.
Examples/示例
Section: Strain your brain!/大腦運動!
> Strings can be tricky sometimes/微妙的字符串 *
1.
>>> a = "some_string"
>>> id(a)140420665652016
>>> id("some" + "_" + "string") # 注意兩個的id值是相同的.
140420665652016
2.
>>> a = "wtf"
>>> b = "wtf"
>>> a is bTrue
>>> a = "wtf!"
>>> b = "wtf!"
>>> a is bFalse
>>> a, b = "wtf!", "wtf!"
>>> a is b
True
3.
很好理解, 對吧?
說明:
這些行為是由於 Cpython 在編譯優化時, 某些情況下會嘗試使用已經存在的不可變對象而不是每次都創建一個新對象。(這種行為被稱作字符串的駐留 [string interning])
發生駐留之後, 許多變量可能指向內存中的相同字符串對象。(從而節省內存)
在上面的代碼中, 字符串是隱式駐留的. 何時發生隱式駐留則取決於具體的實現. 這裡有一些方法可以用來猜測字符串是否會被駐留:
所有長度為 0 和長度為 1 的字符串都被駐留。
字符串在編譯時被實現('wtf' 將被駐留, 但是 ''.join(['w', 't', 'f'] 將不會被駐留)
字符串中只包含字母,數字或下劃線時將會駐留. 所以 'wtf!' 由於包含 ! 而未被駐留. 可以在這裡找到 CPython 對此規則的實現。
當在同一行將 a 和 b 的值設置為 "wtf!" 的時候, Python 解釋器會創建一個新對象, 然後同時引用第二個變量。如果你在不同的行上進行賦值操作,它就不會「知道」已經有一個 wtf!對象 (因為 "wtf!" 不是按照上面提到的方式被隱式駐留的). 它是一種編譯器優化, 特別適用於交互式環境。
常量摺疊 (constant folding) 是 Python 中的一種 窺孔優化 (peephole optimization) 技術。這意味著在編譯時表達式 'a'*20 會被替換為 'aaaaaaaaaaaaaaaaaaaa' 以減少運行時的時鐘週期。只有長度小於 20 的字符串才會發生常量摺疊。(為啥? 想象一下由於表達式 'a'*10**10 而生成的 .pyc 文件的大小)。相關的源碼實現在這裡(https://github.com/python/cpython/blob/3.6/Python/peephole.c#L288)。
> Time for some hash brownies!/是時候來點蛋糕了!
hash brownie 指一種含有大麻成分的蛋糕, 所以這裡是句雙關
1.
some_dict = {} some_dict[5.5] = "Ruby"
some_dict[5.0] = "JavaScript"
some_dict[5] = "Python"
Output:
"Python" 消除了 "JavaScript" 的存在?
說明:
Python 字典通過檢查鍵值是否相等和比較哈希值來確定兩個鍵是否相同。
具有相同值的不可變對象在 Python 中始終具有相同的哈希值。
注意: 具有不同值的對象也可能具有相同的哈希值(哈希衝突).
當執行 some_dict[5] = "Python" 語句時, 因為 Python 將 5 和 5.0 識別為 some_dict 的同一個鍵, 所以已有值 "JavaScript" 就被 "Python" 覆蓋了.
這個 StackOverflow 的回答(https://stackoverflow.com/a/32211042/4354153)漂亮的解釋了這背後的基本原理.
> Return return everywhere!/到處返回!
def some_func:
try:
return 'from_try' finally:
return 'from_finally'
Output:
說明:
當在 "try...finally" 語句的 try 中執行 return, break 或 continue 後,finally 子句依然會執行。
函數的返回值由最後執行的 return 語句決定。由於 finally 子句一定會執行,所以 finally 子句中的 return 將始終是最後執行的語句。
> Deep down, we're all the same./本質上, 我們都一樣. *
class WTF:
pass
Output:
>>> WTF == WTF # 兩個不同的對象應該不相等
False
>>> WTF is WTF # 也不相同
False
>>> hash(WTF) == hash(WTF) # 哈希值也應該不同
True
>>> id(WTF) == id(WTF)
True
說明:
當調用 id 函數時,Python 創建了一個 WTF 類的對象並傳給 id 函數。然後 id 函數獲取其 id 值 (也就是內存地址),然後丟棄該對象。該對象就被銷燬了。
當我們連續兩次進行這個操作時,Python 會將相同的內存地址分配給第二個對象。因為 (在 CPython 中) id 函數使用對象的內存地址作為對象的 id 值,所以兩個對象的 id 值是相同的。
綜上,對象的 id 值僅僅在對象的生命週期內唯一。在對象被銷燬之後,或被創建之前,其他對象可以具有相同的 id 值。
那為什麼 is 操作的結果為 False 呢?讓我們看看這段代碼。
class WTF(object):
def __init__(self): print("I")
def __del__(self): print("D")
Output:
正如你所看到的, 對象銷燬的順序是造成所有不同之處的原因.
> For what?/為什麼?
some_string = "wtf"
some_dict = {}
for i, some_dict[i] in enumerate(some_string):
pass
Output:
說明:
Python 語法 中對 for 的定義是:
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
其中 exprlist 指分配目標。這意味著對可迭代對象中的每一項都會執行類似 {exprlist} = {next_value} 的操作。
一個有趣的例子說明了這一點:
for i in range(4):
print(i) i = 10
Output:
0 1 2 3
你可曾覺得這個循環只會運行一次?
說明:
由於循環在 Python 中工作方式,賦值語句 i = 10 並不會影響迭代循環,在每次迭代開始之前,迭代器 (這裡指 range(4)) 生成的下一個元素就被解包並賦值給目標列表的變量 (這裡指 i) 了。
在每一次的迭代中, enumerate(some_string) 函數就生成一個新值 i (計數器增加) 並從 some_string 中獲取一個字符. 然後將字典 some_dict 鍵 i (剛剛分配的) 的值設為該字符. 本例中循環的展開可以簡化為:
>>> i, some_dict[i] = (0, 'w')
>>> i, some_dict[i] = (1, 't')
>>> i, some_dict[i] = (2, 'f')
>>> some_dict
> Evaluation time discrepancy/評估時間差異
1.
array = [1, 8, 15] g = (x for x in array if array.count(x) > 0) array = [2, 8, 22]
Output:
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:
說明
在生成器表達式中,in 子句在聲明時執行,而條件子句則是在運行時執行。
所以在運行前,array 已經被重新賦值為 [2, 8, 22],因此對於之前的 1, 8 和 15, 只有 count(8) 的結果是大於 0 的,所以生成器只會生成 8。
第二部分中 g1 和 g2 的輸出差異則是由於變量 array_1 和 array_2 被重新賦值的方式導致的。
在第一種情況下,array_1 被綁定到新對象 [1,2,3,4,5],因為 in 子句是在聲明時被執行的,所以它仍然引用舊對象 [1,2,3,4](並沒有被銷燬)。
在第二種情況下,對 array_2 的切片賦值將相同的舊對象 [1,2,3,4] 原地更新為 [1,2,3,4,5]。因此 g2 和 array_2 仍然引用同一個對象 (這個對象現在已經更新為 [1,2,3,4,5])。
> is is not what it is!/出人意料的 is!
下面是一個在互聯網上非常有名的例子.
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
>>> a = 257; b = 257
>>> a is b
True
說明:
is 和 == 的區別
is 運算符檢查兩個運算對象是否引用自同一對象 (即, 它檢查兩個預算對象是否相同)。
== 運算符比較兩個運算對象的值是否相等。
因此 is 代表引用相同,== 代表值相等。下面的例子可以很好的說明這點,
256 是一個已經存在的對象, 而 257 不是
當你啟動 Python 的時候, -5 到 256 的數值就已經被分配好了。這些數字因為經常使用所以適合被提前準備好。
引用自 https://docs.python.org/3/c-api/long.html
當前的實現為-5 到 256 之間的所有整數保留一個整數對象數組, 當你創建了一個該範圍內的整數時, 你只需要返回現有對象的引用. 所以改變 1 的值是有可能的. 我懷疑這種行為在 Python 中是未定義行為. :-)
>>> id(256)
10922528
>>> a = 256
>>> b = 256
>>> id(a)
10922528
>>> id(b)
10922528
>>> id(257)
140084850247312
>>> x = 257
>>> y = 257
>>> id(x)
140084850247440
>>> id(y)
140084850247344
這裡解釋器並沒有智能到能在執行 y = 257 時意識到我們已經創建了一個整數 257,所以它在內存中又新建了另一個對象。
當 a 和 b 在同一行中使用相同的值初始化時,會指向同一個對象。
>>> a, b = 257, 257
>>> id(a)
140640774013296
>>> id(b)
140640774013296
>>> a = 257
>>> b = 257
>>> id(a)
140640774013392
>>> id(b)
140640774013488
當 a 和 b 在同一行中被設置為 257 時, Python 解釋器會創建一個新對象, 然後同時引用第二個變量. 如果你在不同的行上進行, 它就不會 "知道" 已經存在一個 257 對象了。
這是一種特別為交互式環境做的編譯器優化. 當你在實時解釋器中輸入兩行的時候, 他們會單獨編譯, 因此也會單獨進行優化. 如果你在 .py 文件中嘗試這個例子, 則不會看到相同的行為, 因為文件是一次性編譯的。
> A tic-tac-toe where X wins in the first attempt!/一蹴即至!
# 我們先初始化一個變量row
row = [""]*3 #row i['', '', '']
# 並創建一個變量board
board = [row]*3
Output:
>>> board [['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0] ['', '', '']
>>> board[0][0]''
>>> board[0][0] = "X"
>>> board [['X', '', ''], ['X', '', ''], ['X', '', '']]
我們有沒有賦值過 3 個 "X" 呢?
說明:
當我們初始化 row 變量時, 下面這張圖展示了內存中的情況。
而當通過對 row 做乘法來初始化 board 時, 內存中的情況則如下圖所示 (每個元素 board[0], board[1] 和 board[2] 都和 row 一樣引用了同一列表.)
我們可以通過不使用變量 row 生成 board 來避免這種情況. (這個 issue 提出了這個需求(https://github.com/satwikkansal/wtfpython/issues/68)
>>> board = [['']*3 for _ in range(3)]
>>> board[0][0] = "X"
>>> board [['X', '', ''], ['', '', ''], ['', '', '']]
> The sticky output function/麻煩的輸出
funcs = results =
for x in range(7):
def some_func:
return x funcs.append(some_func) results.append(some_func) funcs_results = [func() for func in funcs]
Output:
即使每次在迭代中將 some_func 加入 funcs 前的 x 值都不相同, 所有的函數還是都返回 6。
// 再換個例子
>>> powers_of_x = [lambda x: x**i for i in range(10)]
>>> [f(2) for f in powers_of_x] [512, 512, 512, 512, 512, 512, 512, 512, 512, 512]
說明:
當在循環內部定義一個函數時,如果該函數在其主體中使用了循環變量,則閉包函數將與循環變量綁定,而不是它的值。因此,所有的函數都是使用最後分配給變量的值來進行計算的。
可以通過將循環變量作為命名變量傳遞給函數來獲得預期的結果。為什麼這樣可行?因為這會在函數內再次定義一個局部變量。
funcs =
for x in range(7):
def some_func(x=x):
return x funcs.append(some_func)
Output:
> is not ... is not is (not ...)/is not ... 不是 is (not ...)
說明:
is not 是個單獨的二進制運算符,和分別使用 is 和 not 不同。
如果操作符兩側的變量指向同一個對象,則 is not 的結果為 False, 否則結果為 True。
本文中,機器之心摘錄了該項目中約 1/4 的例子,更多內容請查看原項目頁:
https://github.com/leisurelicht/wtfpython-cn
點擊「閱讀原文」填寫報名表單,工作人員在審核後會發送評選申請表格。
申請企業需按照要求填寫評選申請表格。
未盡事宜可添加機器之心小助手:syncedai3 進行諮詢;添加好友請備註「年度評選」。
閱讀更多 坤艮機器之心 的文章