天天看點

超8千Star,火遍Github的Python反直覺案例集!

大資料文摘授權轉載

作者:Satwik Kansal

譯者:暮晨

Python,是一個設計優美的解釋型進階語言,它提供了很多能讓程式員感到舒适的功能特性。

但有的時候,Python的一些輸出結果對于初學者來說似乎并不是那麼一目了然。

這個有趣的項目意在收集Python中那些難以了解和反人類直覺的例子以及鮮為人知的功能特性,并嘗試讨論這些現象背後真正的原理!

雖然下面的有些例子并不一定會讓你覺得WTFs,但它們依然有可能會告訴你一些你所不知道的Python有趣特性。我覺得這是一種學習程式設計語言内部原理的好辦法,而且我相信你也會從中獲得樂趣!

如果你是一位經驗比較豐富的Python程式員,你可以嘗試挑戰看是否能一次就找到例子的正确答案。你可能對其中的一些例子已經比較熟悉了,那這也許能喚起你當年踩這些坑時的甜蜜回憶

超8千Star,火遍Github的Python反直覺案例集!

示例結構

所有示例的結構都如下所示:

> 一個精選的标題 *

标題末尾的星号表示該示例在第一版中不存在,是最近添加的。

# 準備代碼.

# 釋放魔法...

Output (Python version):

>>> 觸發語句

出乎意料的輸出結果

(可選): 對意外輸出結果的簡短描述。

說明:

簡要說明發生了什麼以及為什麼會發生。

如有必要,舉例說明

Output:

>>>觸發語句#一些讓魔法變得容易了解的例子

#一些正常的輸入

注意:所有的示例都在Python3.5.2版本的互動解釋器上測試過,如果不特别說明應該适用于所有Python版本。

用法

我個人建議,最好依次閱讀下面的示例,并對每個示例:

仔細閱讀設定例子最開始的代碼。如果您是一位經驗豐富的 Python 程式員,那麼大多數時候您都能成功預期到後面的結果。

閱讀輸出結果

  • 确認結果是否如你所料.
  • 确認你是否知道這背後的原理

PS: 你也可以在指令行閱讀 WTFpython. 我們有 pypi 包 和 npm 包(支援代碼高亮).(譯: 這兩個都是英文版的)

安裝 npm 包 wtfpython

$ npm install -g wtfpython           

或者, 安裝 pypi 包 wtfpython

$ pip install wtfpython -U           

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

示例

大腦運動!

微妙的字元串*

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

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

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

  • 所有長度為 0 和長度為 1 的字元串都被駐留。
  • 字元串在編譯時被實作 ('wtf' 将被駐留, 但是 ''.join(['w', 't', 'f'] 将不會被駐留)
  • 字元串中隻包含字母,數字或下劃線時将會駐留。是以 'wtf!' 由于包含!而未被駐留。可以在這裡找CPython對此規則的實作。
超8千Star,火遍Github的Python反直覺案例集!

當在同一行将 a 和 b 的值設定為 "wtf!" 的時候, Python 解釋器會建立一個新對象, 然後同時引用第二個變量. 如果你在不同的行上進行指派操作, 它就不會“知道”已經有一個 wtf! 對象 (因為 "wtf!" 不是按照上面提到的方式被隐式駐留的)。它是一種編譯器優化,特别适用于互動式環境。

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

https://github.com/leisurelicht/wtfpython-cn/blob/master/images/string-intern/string_intern.png

是時候來點蛋糕了!

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           

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

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

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

到處傳回!

def some_func():
 try:
 return 'from_try'
 finally:
 return 'from_finally'           
>>> some_func()

'from_finally'
           

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

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

本質上,我們都一樣. *

class WTF:
 pass           
>>> 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")           
>>> 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
           
>>> some_dict # 建立了索引字典.
{0: 'w', 1: 't', 2: 'f'}
           

Python 文法 中對 for 的定義是:

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

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

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

for i in range(4):
 print(i)
 i = 10           
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           

評估時間差異

array = [1, 8, 15]
g = (x for x in array if array.count(x) > 0)
array = [2, 8, 22]

           
>>> print(list(g))
[8]           
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]
           
>>> 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]).

由于字數和排版受限,感興趣的同學請自行前往GitHub連結檢視原碼。

原文連結:

https://github.com/satwikkansal/wtfpython

中文版:

https://github.com/leisurelicht/wtfpython-cn#section-strain-your-brain%E5%A4%A7%E8%84%91%E8%BF%90%E5%8A%A8

原文釋出時間為:2018-11-30

本文作者:大資料文摘

本文來自雲栖社群合作夥伴“

大資料文摘

”,了解相關資訊可以關注“BigDataDigest”微信公衆号