天天看點

Python帶我飛:50個有趣而又鮮為人知的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 not is (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

或者, 安裝 pypi 包 wtfpython

現在, 在指令行中運作 wtfpython, 你就可以開始浏覽了。

小标題:Examples/示例

1、

2、

3、

很好了解, 對吧?

說明:

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

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

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

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

字元串在編譯時被實作 ('wtf' 将被駐留, 但是 ''.join(['w', 't', 'f'] 将不會被駐留)

字元串中隻包含字母,數字或下劃線時将會駐留. 是以 'wtf!' 由于包含 ! 而未被駐留. 可以在這裡找到 CPython 對此規則的實作。

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

當在同一行将 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指一種含有大麻成分的蛋糕, 是以這裡是句雙關

Output:

"Python" 消除了 "JavaScript" 的存在?

Python 字典通過檢查鍵值是否相等和比較哈希值來确定兩個鍵是否相同。

具有相同值的不可變對象在Python中始終具有相同的哈希值。

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

當執行 some_dict[5] = "Python" 語句時,因為Python将 5 和 5.0 識别為 some_dict 的同一個鍵, 是以已有值 "JavaScript" 就被 "Python" 覆寫了。

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

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

函數的傳回值由最後執行的 return 語句決定. 由于 finally 子句一定會執行, 是以 finally 子句中的 return 将始終是最後執行的語句。

當調用 id 函數時, Python 建立了一個 WTF 類的對象并傳給 id 函數。然後 id 函數擷取其id值 (也就是記憶體位址), 然後丢棄該對象。該對象就被銷毀了。

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

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

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

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

Python 文法 中對 for 的定義是:

其中 exprlist 指配置設定目标. 這意味着對可疊代對象中的每一項都會執行類似 {exprlist} = {next_value} 的操作.

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

你可曾覺得這個循環隻會運作一次?

由于循環在Python中工作方式, 指派語句 i = 10 并不會影響疊代循環, 在每次疊代開始之前, 疊代器(這裡指 range(4)) 生成的下一個元素就被解包并指派給目标清單的變量(這裡指 i)了.

在每一次的疊代中, enumerate(some_string) 函數就生成一個新值 i (計數器增加) 并從 some_string 中擷取一個字元. 然後将字典 some_dict 鍵 i (剛剛配置設定的) 的值設為該字元. 本例中循環的展開可以簡化為:

說明

在生成器表達式中, 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 運算符檢查兩個運算對象是否引用自同一對象 (即, 它檢查兩個預算對象是否相同).

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

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

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

當你啟動Python 的時候, -5 到 256 的數值就已經被配置設定好了. 這些數字因為經常使用是以适合被提前準備好。

這裡解釋器并沒有智能到能在執行 y = 257 時意識到我們已經建立了一個整數 257, 是以它在記憶體中又建立了另一個對象。

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

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

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

我們有沒有指派過3個 "X" 呢?

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

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

而當通過對 row 做乘法來初始化 board 時, 記憶體中的情況則如下圖所示 (每個元素 board[0], board[1] 和 board[2] 都和 row 一樣引用了同一清單。)

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

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

即使每次在疊代中将 some_func 加入 funcs 前的 x 值都不相同, 所有的函數還是都傳回6。

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

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

is not 是個單獨的二進制運算符, 和分别使用 is 和 not 不同。

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

更多内容請看原文連結:

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

原文釋出時間為:2018-12-1

本文作者:新智元

本文來自雲栖社群合作夥伴新智元,了解相關資訊可以關注“AI_era”微信公衆号

原文連結:

https://mp.weixin.qq.com/s/K3UNipJaylDzweL3pBnSSQ