尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接
新智元推薦
來源:GitHub
編輯:三石
【新智元導讀】這個有趣的項目意在收集 Python 中那些難以理解和反人類直覺的例子,以及鮮為人知的功能特性, 並嘗試討論這些現象背後真正的原理。資深Python 工程師可以嘗試挑戰,看是否能一次就找到例子的正確答案,也許能喚起你當年踩這些坑時的甜蜜回憶。
Python, 是一個設計優美的解釋型高級語言, 它提供了很多能讓工程師感到舒適的功能特性。但有的時候, Python 的一些輸出結果對於初學者來說似乎並不是那麼一目了然。
這個有趣的項目意在收集 Python 中那些難以理解和反人類直覺的例子以及鮮為人知的功能特性, 並嘗試討論這些現象背後真正的原理!
雖然下面的有些例子並不一定會讓你覺得 WTFs,但它們依然有可能會告訴你一些你所不知道的 Python 有趣特性。我覺得這是一種學習編程語言內部原理的好辦法, 而且我相信你也會從中獲得樂趣!
目錄
-
Structure of the Examples/示例結構
-
Usage/用法
-
?Examples/示例
-
>
+=
is faster/更快的+=
-
> Let’s make a giant string!/來做個巨大的字符串吧!
-
> Explicit typecast of strings/字符串的顯式類型轉換
-
> Minor Ones/小知識點
-
> Okay Python, Can you make me fly?/Python, 可否帶我飛? *
-
>
goto
, but why?/goto
, 但為什麼? * -
> Brace yourself!/做好思想準備 *
-
> Let’s meet Friendly Language Uncle For Life/讓生活更友好 *
-
> Even Python understands that love is complicated/連Python也知道愛是難言的 *
-
> Yes, it exists!/是的, 它存在!
-
> Inpinity/無限 *
-
> Mangling time!修飾時間! *
-
> Modifying a dictionary while iterating over it/迭代字典時的修改
-
> Stubborn
del
operator/堅強的del
* -
> Deleting a list item while iterating/迭代列表時刪除元素
-
> Loop variables leaking out!/循環變量泄漏!
-
> Beware of default mutable arguments!/當心默認的可變參數!
-
> Catching the Exceptions/捕獲異常
-
> Same operands, different story!/同人不同命!
-
> The out of scope variable/外部作用域變量
-
> Be careful with chained operations/小心鏈式操作
-
> Name resolution ignoring class scope/忽略類作用域的名稱解析
-
> Needle in a Haystack/大海撈針
-
> Skipping lines?/跳過一行?
-
> Teleportation/空間移動 *
-
> Well, something is fishy…/嗯, 有些可疑…
-
> Strings can be tricky sometimes/微妙的字符串 *
-
> Time for some hash brownies!/是時候來點蛋糕了!
-
> Return return everywhere!/到處返回!
-
> Deep down, we’re all the same./本質上,我們都一樣. *
-
> For what?/為什麼?
-
> Evaluation time discrepancy/評估時間差異
-
>
is
is not what it is!/出人意料的is
! -
> A tic-tac-toe where X wins in the first attempt!/一蹴即至!
-
> The sticky output function/麻煩的輸出
-
>
is not ...
is notis (not ...)
/is not ...
不是is (not ...)
-
> The surprising comma/意外的逗號
-
> Backslashes at the end of string/字符串末尾的反斜杠
-
> not knot!/別糾結!
-
> Half triple-quoted strings/三個引號
-
> Midnight time doesn’t exist?/不存在的午夜?
-
> What’s wrong with booleans?/布爾你怎麼了?
-
> Class attributes and instance attributes/類屬性和實例屬性
-
> yielding None/生成 None
-
> Mutating the immutable!/強人所難
-
> The disappearing variable from outer scope/消失的外部變量
-
> When True is actually False/真亦假
-
> From filled to None in one instruction…/從有到無…
-
> Subclass relationships/子類關係 *
-
> The mysterious key type conversion/神秘的鍵型轉換 *
-
> Let’s see if you can guess this?/看看你能否猜到這一點?
-
Section: Strain your brain!/大腦運動!
-
Section: Appearances are deceptive!/外表是靠不住的!
-
Section: Watch out for the landmines!/小心地雷!
-
Section: The Hidden treasures!/隱藏的寶藏!
-
Section: Miscellaneous/雜項
-
Contributing/貢獻
-
Acknowledgements/致謝
-
License/許可
-
Help/幫助
-
Want to surprise your geeky pythonist friends?/想給你的極客朋友一個驚喜?
-
Need a pdf version?/需要來一份pdf版的?
-
Follow Commit/追蹤Commit
示例結構
所有示例的結構都如下所示:
> 一個精選的標題 *
標題末尾的星號表示該示例在第一版中不存在,是最近添加的。
#準備代碼.#釋放魔法...
Output (Python version):
>>>觸發語句出乎意料的輸出結果
(可選): 對意外輸出結果的簡短描述。
說明:
簡要說明發生了什麼以及為什麼會發生。
如有必要,舉例說明
Output:
>>>觸發語句#一些讓魔法變得容易理解的例子#一些正常的輸入
注意:所有的示例都在 Python 3.5.2 版本的交互解釋器上測試過, 如果不特別說明應該適用於所有 Python 版本。
小標題:Usage/用法
我個人建議, 最好依次閱讀下面的示例, 並對每個示例:
-
仔細閱讀設置例子最開始的代碼. 如果您是一位經驗豐富的 Python 工程師, 那麼大多數時候您都能成功預期到後面的結果。
-
閱讀輸出結果,
-
如果不知道, 深呼吸然後閱讀說明 (如果你還是看不明白, 別沉默! 可以在這提個 issue)。
-
如果知道, 給自己點獎勵, 然後去看下一個例子。
-
確認結果是否如你所料。
-
確認你是否知道這背後的原理。
PS: 你也可以在命令行閱讀 WTFpython. 我們有 pypi 包 和 npm 包(支持代碼高亮)。(譯: 這兩個都是英文版的)
安裝 npm 包wtfpython
$npminstall-gwtfpython
或者, 安裝 pypi 包wtfpython
$pipinstallwtfpython-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">>>aisbTrue>>>a="wtf!">>>b="wtf!">>>aisbFalse>>>a,b="wtf!","wtf!">>>aisbTrue
3、
>>>'a'*20is'aaaaaaaaaaaaaaaaaaaa'True>>>'a'*21is'aaaaaaaaaaaaaaaaaaaaa'False
很好理解, 對吧?
說明:
-
這些行為是由於 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
文件的大小). 相關的源碼做到在這里。
>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:
>>>some_dict[5.5]"Ruby">>>some_dict[5.0]"Python">>>some_dict[5]"Python"
“Python” 消除了 “JavaScript” 的存在?
說明:
-
Python 字典通過檢查鍵值是否相等和比較哈希值來確定兩個鍵是否相同。
-
具有相同值的不可變對象在Python中始終具有相同的哈希值。
>>>5==5.0True>>>hash(5)==hash(5.0)True
-
注意:具有不同值的對象也可能具有相同的哈希值(哈希衝突)。
-
當執行
some_dict[5] = "Python"
語句時,因為Python將5
和5.0
識別為some_dict
的同一個鍵, 所以已有值 “JavaScript” 就被 “Python” 覆蓋了。 -
這個 StackOverflow的回答漂亮的解釋了這背後的基本原理。
> Return return everywhere!/到處返回!
defsome_func():try:return'from_try'finally:return'from_finally'
Output:
>>>some_func()'from_finally'
說明:
-
當在 “try…finally” 語句的
try
中執行return
,break
或continue
後,finally
子句依然會執行。 -
函數的返回值由最後執行的
return
語句決定. 由於finally
子句一定會執行, 所以finally
子句中的return
將始終是最後執行的語句。
> Deep down, we’re all the same./本質上,我們都一樣. *
classWTF:pass
Output:
>>>WTF()==WTF()#兩個不同的對象應該不相等False>>>WTF()isWTF()#也不相同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
呢? 讓我們看看這段代碼。
classWTF(object):def__init__(self):print("I")def__del__(self):print("D")
Output:
>>>WTF()isWTF()IIDDFalse>>>id(WTF())==id(WTF())IDIDTrue
正如你所看到的, 對象銷毀的順序是造成所有不同之處的原因。
> For what?/為什麼?
some_string="wtf"some_dict={}fori,some_dict[i]inenumerate(some_string):pass
Output:
>>>some_dict#創建了索引字典.{0:'w',1:'t',2:'f'}
說明:
-
Python 語法中對
for
的定義是:
for_stmt:'for'exprlist'in'testlist':'suite['else'':'suite]
其中exprlist
指分配目標. 這意味著對可迭代對象中的每一項都會執行類似{exprlist} = {next_value}
的操作.
一個有趣的例子說明了這一點:
foriinrange(4):print(i)i=10
Output:
0123
你可曾覺得這個循環只會運行一次?
說明:
-
由於循環在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=(xforxinarrayifarray.count(x)>0)array=[2,8,22]
Output:
>>>print(list(g))[8]
2、
array_1=[1,2,3,4]g1=(xforxinarray_1)array_1=[1,2,3,4,5]array_2=[1,2,3,4]g2=(xforxinarray_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
子句在聲明時執行, 而條件子句則是在運行時執行。 -
所以在運行前,
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]
)。
>isis not what it is!/出人意料的is!
下面是一個在互聯網上非常有名的例子。
>>>a=256>>>b=256>>>aisbTrue>>>a=257>>>b=257>>>aisbFalse>>>a=257;b=257>>>aisbTrue
說明:
is
和==
的區別
-
is
運算符檢查兩個運算對象是否引用自同一對象 (即, 它檢查兩個預算對象是否相同). -
==
運算符比較兩個運算對象的值是否相等. -
因此
is
代表引用相同,==
代表值相等. 下面的例子可以很好的說明這點,
>>>[]==[]True>>>[]is[]#這兩個空列表位於不同的內存地址.False
256
是一個已經存在的對象, 而257
不是
當你啟動Python 的時候,-5
到256
的數值就已經被分配好了. 這些數字因為經常使用所以適合被提前準備好。
>>>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!/一蹴即至!
Output:
#我們先初始化一個變量rowrow=[""]*3#rowi['','','']#並創建一個變量boardboard=[row]*3
我們有沒有賦值過3個 “X” 呢?
>>>board[['','',''],['','',''],['','','']]>>>board[0]['','','']>>>board[0][0]''>>>board[0][0]="X">>>board[['X','',''],['X','',''],['X','','']]
說明:
當我們初始化row
變量時, 下面這張圖展示了內存中的情況。
而當通過對row
做乘法來初始化board
時, 內存中的情況則如下圖所示 (每個元素board[0]
,board[1]
和board[2]
都和row
一樣引用了同一列表。)
我們可以通過不使用變量row
生成board
來避免這種情況. (這個issue提出了這個需求。)
>>>board=[['']*3for_inrange(3)]>>>board[0][0]="X">>>board[['X','',''],['','',''],['','','']]
> The sticky output function/麻煩的輸出
funcs=[]results=[]forxinrange(7):defsome_func():returnxfuncs.append(some_func)results.append(some_func())funcs_results=[func()forfuncinfuncs]
Output:
>>>results[0,1,2,3,4,5,6]>>>funcs_results[6,6,6,6,6,6,6]
即使每次在迭代中將some_func
加入funcs
前的x
值都不相同, 所有的函數還是都返回6。
說明:
-
當在循環內部定義一個函數時, 如果該函數在其主體中使用了循環變量, 則閉包函數將與循環變量綁定, 而不是它的值. 因此, 所有的函數都是使用最後分配給變量的值來進行計算的.
-
可以通過將循環變量作為命名變量傳遞給函數來獲得預期的結果.為什麼這樣可行?因為這會在函數內再次定義一個局部變量。
funcs=[]forxinrange(7):defsome_func(x=x):returnxfuncs.append(some_func)
Output:
>>>funcs_results=[func()forfuncinfuncs]>>>funcs_results[0,1,2,3,4,5,6]
>is not ...
is notis (not ...)
/is not ...
不是is (not ...)
>>>'something'isnotNoneTrue>>>'something'is(notNone)False
說明:
-
is not
是個單獨的二進制運算符, 和分別使用is
和not
不同。 -
如果操作符兩側的變量指向同一個對象, 則
is not
的結果為False
, 否則結果為True。
更多內容請看原文鏈接:
https://github.com/leisurelicht/wtfpython-cn
【加入社群】
新智元 AI 技術 + 產業社群招募中,歡迎對 AI 技術 + 產業落地感興趣的同學,加小助手微信號:aiera2015_2入群;通過審核後我們將邀請進群,加入社群後務必修改群備註(姓名 – 公司 – 職位;專業群審核較嚴,敬請諒解)。