每日最新頭條.有趣資訊

Python帶我飛:50個有趣而又鮮為人知的Python特性

新智元推薦

來源: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, 可否帶我飛? *

>, but why?/, 但為什麽? *

> 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/迭代字典時的修改

> Stubbornoperator/堅強的*

> 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 not what it is!/出人意料的!

> A tic-tac-toe where X wins in the first attempt!/一蹴即至!

> The sticky output function/麻煩的輸出

>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 包

$ npm install -g wtfpython

或者, 安裝 pypi 包

$ 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"

>>> aisb

True

>>> a ="wtf!"

>>> b ="wtf!"

>>> aisb

False

>>> a, b ="wtf!","wtf!"

>>> aisb

True

3、

>>>'a'*20is'aaaaaaaaaaaaaaaaaaaa'

True

>>>'a'*21is'aaaaaaaaaaaaaaaaaaaaa'

False

很好理解, 對吧?

說明:

這些行為是由於 Cpython 在編譯優化時, 某些情況下會嘗試使用已經存在的不可變對象而不是每次都創建一個新對象. (這種行為被稱作字元串的駐留[string interning])

發生駐留之後, 許多變量可能指向記憶體中的相同字元串對象。 (從而節省記憶體)

在上面的代碼中, 字元串是隱式駐留的. 何時發生隱式駐留則取決於具體的實現。這裡有一些方法可以用來猜測字元串是否會被駐留:

所有長度為 0 和長度為 1 的字元串都被駐留。

字元串在編譯時被實現 (將被駐留, 但是將不會被駐留)

字元串中隻包含字母,數字或下劃線時將會駐留. 所以由於包含而未被駐留. 可以在這裡找到 CPython 對此規則的實現。

當在同一行將和的值設定為的時候, Python 解釋器會創建一個新對象, 然後同時引用第二個變量. 如果你在不同的行上進行賦值操作, 它就不會“知道”已經有一個對象 (因為不是按照上面提到的方式被隱式駐留的). 它是一種編譯器優化, 特別適用於互動式環境.

常量折疊(constant folding) 是 Python 中的一種 窺孔優化(peephole optimization) 技術. 這意味著在編譯時表達式會被替換為以減少運行時的時鐘周期. 只有長度小於 20 的字元串才會發生常量折疊。(為啥? 想象一下由於表達式而生成的檔案的大小). 相關的源碼實現在這裡。

>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.0

True

>>> hash(5) == hash(5.0)

True

注意:具有不同值的對象也可能具有相同的哈希值(哈希衝突)。

當執行語句時,因為Python將和識別為的同一個鍵, 所以已有值 "JavaScript" 就被 "Python" 覆蓋了。

這個 StackOverflow的回答漂亮的解釋了這背後的基本原理。

> Return return everywhere!/到處返回!

defsome_func():

try:

return'from_try'

finally:

return'from_finally'

Output:

>>> some_func()

'from_finally'

說明:

當在 "try...finally" 語句的中執行,或後,子句依然會執行。

函數的返回值由最後執行的語句決定. 由於子句一定會執行, 所以子句中的將始終是最後執行的語句。

> 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

說明:

當調用函數時, Python 創建了一個類的對象並傳給函數。然後函數獲取其id值 (也就是記憶體地址), 然後丟棄該對象。該對象就被銷毀了。

當我們連續兩次進行這個操作時, Python會將相同的記憶體地址分配給第二個對象。因為 (在CPython中)函數使用對象的記憶體地址作為對象的id值, 所以兩個對象的id值是相同的。

綜上, 對象的id值僅僅在對象的生命周期內唯一. 在對象被銷毀之後, 或被創建之前, 其他對象可以具有相同的id值。

那為什麽操作的結果為呢? 讓我們看看這段代碼。

classWTF(object):

def__init__(self):print("I")

def__del__(self):print("D")

Output:

>>> WTF()isWTF()

I

I

D

D

False

>>> id(WTF()) == id(WTF())

I

D

I

D

True

正如你所看到的, 對象銷毀的順序是造成所有不同之處的原因。

> For what?/為什麽?

some_string ="wtf"

some_dict = {}

fori, some_dict[i]inenumerate(some_string):

pass

Output:

>>> some_dict# 創建了索引字典.

{:'w',1:'t',2:'f'}

說明:

Python 語法 中對的定義是:

for_stmt:'for'exprlist'in'testlist':'suite ['else'':'suite]

其中指分配目標. 這意味著對可迭代對象中的每一項都會執行類似的操作.

一個有趣的例子說明了這一點:

foriinrange(4):

print(i)

i =10

Output:

1

2

3

你可曾覺得這個循環只會運行一次?

說明:

由於循環在Python中工作方式, 賦值語句並不會影響迭代循環, 在每次迭代開始之前, 迭代器(這裡指) 生成的下一個元素就被解包並賦值給目標列表的變量(這裡指)了.

在每一次的迭代中,函數就生成一個新值(計數器增加) 並從中獲取一個字元. 然後將字典鍵(剛剛分配的) 的值設為該字元. 本例中循環的展開可以簡化為:

>>> i, some_dict[i] = (,'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) >)

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]

說明

在生成器表達式中,子句在聲明時執行, 而條件子句則是在運行時執行。

所以在運行前,已經被重新賦值為, 因此對於之前的,和, 只有的結果是大於的, 所以生成器只會生成

第二部分中和的輸出差異則是由於變量和被重新賦值的方式導致的。

在第一種情況下,被綁定到新對象, 因為子句是在聲明時被執行的, 所以它仍然引用舊對象(並沒有被銷毀)。

在第二種情況下, 對的切片賦值將相同的舊對象原地更新為. 因此和仍然引用同一個對象(這個對象現在已經更新為)。

> is is not what it is!/出人意料的is!

下面是一個在互聯網上非常有名的例子。

>>> a =256

>>> b =256

>>> aisb

True

>>> a =257

>>> b =257

>>> aisb

False

>>> a =257; b =257

>>> aisb

True

說明:

和 的區別

運算符檢查兩個運算對象是否引用自同一對象 (即, 它檢查兩個預算對象是否相同).

運算符比較兩個運算對象的值是否相等.

因此代表引用相同,代表值相等. 下面的例子可以很好的說明這點,

>>> [] == []

True

>>> []is[]# 這兩個空列表位於不同的記憶體地址.

False

是一個已經存在的對象, 而 不是

當你啟動Python 的時候,到的數值就已經被分配好了. 這些數字因為經常使用所以適合被提前準備好。

這裡解釋器並沒有智能到能在執行時意識到我們已經創建了一個整數, 所以它在記憶體中又新建了另一個對象。

當 和 在同一行中使用相同的值初始化時,會指向同一個對象。

當 a 和 b 在同一行中被設定為時, Python 解釋器會創建一個新對象, 然後同時引用第二個變量. 如果你在不同的行上進行, 它就不會 "知道" 已經存在一個對象了。

這是一種特別為互動式環境做的編譯器優化. 當你在實時解釋器中輸入兩行的時候, 他們會單獨編譯, 因此也會單獨進行優化. 如果你在檔案中嘗試這個例子, 則不會看到相同的行為, 因為檔案是一次性編譯的。

> A tic-tac-toe where X wins in the first attempt!/一蹴即至!

Output:

# 我們先初始化一個變量row

row = [""]*3#row i['', '', '']

# 並創建一個變量board

board = [row]*3

我們有沒有賦值過3個 "X" 呢?

>>> board

[['','',''], ['','',''], ['','','']]

>>> board[]

['','','']

>>> board[][]

''

>>> board[][] ="X"

>>> board

[['X','',''], ['X','',''], ['X','','']]

說明:

當我們初始化變量時, 下面這張圖展示了記憶體中的情況。

而當通過對做乘法來初始化時, 記憶體中的情況則如下圖所示 (每個元素,和都和一樣引用了同一列表。)

我們可以通過不使用變量生成來避免這種情況. (這個issue提出了這個需求。)

>>> board = [['']*3for_inrange(3)]

>>> board[][] ="X"

>>> board

[['X','',''], ['','',''], ['','','']]

> The sticky output function/麻煩的輸出

funcs = []

results = []

forxinrange(7):

defsome_func():

returnx

funcs.append(some_func)

results.append(some_func())

funcs_results = [func()forfuncinfuncs]

Output:

>>> results

[,1,2,3,4,5,6]

>>> funcs_results

[6,6,6,6,6,6,6]

即使每次在迭代中將加入前的值都不相同, 所有的函數還是都返回6。

說明:

當在循環內部定義一個函數時, 如果該函數在其主體中使用了循環變量, 則閉包函數將與循環變量綁定, 而不是它的值. 因此, 所有的函數都是使用最後分配給變量的值來進行計算的.

可以通過將循環變量作為命名變量傳遞給函數來獲得預期的結果.為什麽這樣可行?因為這會在函數內再次定義一個局部變量。

funcs = []

forxinrange(7):

defsome_func(x=x):

returnx

funcs.append(some_func)

Output:

>>> funcs_results = [func()forfuncinfuncs]

>>> funcs_results

[,1,2,3,4,5,6]

>is not/不是

>>>'something'isnotNone

True

>>>'something'is(notNone)

False

說明:

是個單獨的二進製運算符, 和分別使用和不同。

如果操作符兩側的變量指向同一個對象, 則的結果為, 否則結果為

https://github.com/leisurelicht/wtfpython-cn

【加入社群】

新智元 AI 技術 + 產業社群招募中,歡迎對 AI 技術 + 產業落地感興趣的同學,加小助手微信號:aiera2015_2入群;通過審核後我們將邀請進群,加入社群後務必修改群備注(姓名 - 公司 - 職位;專業群審核較嚴,敬請諒解)。

獲得更多的PTT最新消息
按讚加入粉絲團