1. 程式人生 > >What the f*ck Python!(中文翻譯版)

What the f*ck Python!(中文翻譯版)

What the f*ck Python! snake

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

一些有趣且鮮為人知的 Python 特性.

WTFPL 2.0 Commit id

Python, 是一個設計優美的解釋型高階語言, 它提供了很多能讓程式設計師感到舒適的功能特性. 但有的時候, Python 的一些輸出結果對於初學者來說似乎並不是那麼一目瞭然.

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

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

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

如果你不是第一次讀了, 你可以在這裡獲取變動內容.

那麼, 讓我們開始吧...

Table of Contents/目錄

Structure of the Examples/示例結構

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

> 一個精選的標題 *

標題末尾的星號表示該示例在第一版中不存在,是最近新增的.

# 準備程式碼.
# 釋放魔法...

Output (Python version):

>>> 觸發語句
出乎意料的輸出結果

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

bulb 說明:

  • 簡要說明發生了什麼以及為什麼會發生.
    如有必要, 舉例說明
    Output:
    >>> 觸發語句 # 一些讓魔法變得容易理解的例子
    # 一些正常的輸入

注意: 所有的示例都在 Python 3.5.2 版本的互動直譯器上測試過, 如果不特別說明應該適用於所有 Python 版本.

Usage/用法

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

  • 仔細閱讀設定例子最開始的程式碼. 如果您是一位經驗豐富的 Python 程式設計師, 那麼大多數時候您都能成功預期到後面的結果.
  • 閱讀輸出結果,
    • 確認結果是否如你所料.
    • 確認你是否知道這背後的原理.
      • 如果不知道, 深呼吸然後閱讀說明 (如果你還是看不明白, 別沉默! 可以在提個 issue).
      • 如果知道, 給自己點獎勵, 然後去看下一個例子.

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

安裝 npm 包 wtfpython

$ npm install -g wtfpython

或者, 安裝 pypi 包 wtfpython

$ pip install wtfpython -U

現在, 在命令列中執行 wtfpython, 你就可以開始瀏覽了.


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

很好理解, 對吧?

bulb 說明:

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

  • 發生駐留之後, 許多變數可能指向記憶體中的相同字串物件. (從而節省記憶體)

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

    • 所有長度為 0 和長度為 1 的字串都被駐留.

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

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

  • 當在同一行將 ab 的值設定為 "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" 的存在?

bulb 說明:

  • Python 字典通過檢查鍵值是否相等和比較雜湊值來確定兩個鍵是否相同.
  • 具有相同值的不可變物件在Python中始終具有相同的雜湊值.
    >>> 5 == 5.0
    True
    >>> hash(5) == hash(5.0)
    True
    注意: 具有不同值的物件也可能具有相同的雜湊值(雜湊衝突).
  • 當執行 some_dict[5] = "Python" 語句時, 因為Python將 55.0 識別為 some_dict 的同一個鍵, 所以已有值 "JavaScript" 就被 "Python" 覆蓋了.
  • 這個 StackOverflow的 回答 漂亮的解釋了這背後的基本原理.

> Return return everywhere!/到處返回!

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

Output:

>>> some_func()
'from_finally'

bulb 說明:

  • 當在 "try...finally" 語句的 try 中執行 return, breakcontinue 後, 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

bulb 說明:

  • 當呼叫 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:

    >>> WTF() is WTF()
    I
    I
    D
    D
    False
    >>> id(WTF()) == id(WTF())
    I
    D
    I
    D
    True

    正如你所看到的, 物件銷燬的順序是造成所有不同之處的原因.


> For what?/為什麼?

some_string = "wtf"
some_dict = {}
for i, some_dict[i] in enumerate(some_string):
    pass

Output:

>>> some_dict # 建立了索引字典.
{0: 'w', 1: 't', 2: 'f'}

bulb 說明:

  • 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
    

    你可曾覺得這個迴圈只會執行一次?

    bulb 說明:

    • 由於迴圈在Python中工作方式, 賦值語句 i = 10 並不會影響迭代迴圈, 在每次迭代開始之前, 迭代器(這裡指 range(4)) 生成的下一個元素就被解包並賦值給目標列表的變數(這裡指 i)了.
  • 在每一次的迭代中, enumerate(some_string) 函式就生成一個新值 i (計數器增加) 並從 some_string 中獲取一個字元. 然後將字典 some_dicti (剛剛分配的) 的值設為該字元. 本例中迴圈的展開可以簡化為:

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

>>> print(list(g))
[8]

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:

>>> print(list(g1))
[1,2,3,4]

>>> print(list(g2))
[1,2,3,4,5]

bulb 說明

  • 生成器表示式中, in 子句在宣告時執行, 而條件子句則是在執行時執行.
  • 所以在執行前, array 已經被重新賦值為 [2, 8, 22], 因此對於之前的 1, 815, 只有 count(8) 的結果是大於 0 的, 所以生成器只會生成 8.
  • 第二部分中 g1g2 的輸出差異則是由於變數 array_1array_2 被重新賦值的方式導致的.
  • 在第一種情況下, array_1 被繫結到新物件 [1,2,3,4,5], 因為 in 子句是在宣告時被執行的, 所以它仍然引用舊物件 [1,2,3,4](並沒有被銷燬).
  • 在第二種情況下, 對 array_2 的切片賦值將相同的舊物件 [1,2,3,4] 原地更新為 [1,2,3,4,5]. 因此 g2array_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

bulb 說明:

is== 的區別

  • is 運算子檢查兩個運算物件是否引用自同一物件 (即, 它檢查兩個預算物件是否相同).
  • == 運算子比較兩個運算物件的值是否相等.
  • 因此 is 代表引用相同, == 代表值相等. 下面的例子可以很好的說明這點,
    >>> [] == []
    True
    >>> [] is [] # 這兩個空列表位於不同的記憶體地址.
    False

256 是一個已經存在的物件, 而 257 不是

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

引用自 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, 所以它在記憶體中又新建了另一個物件.

ab 在同一行中使用相同的值初始化時,會指向同一個物件.

>>> 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" 呢?

bulb 說明:

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

image

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

image

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

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

>>> results
[0, 1, 2, 3, 4, 5, 6]
>>> funcs_results
[6, 6, 6, 6, 6, 6, 6]

即使每次在迭代中將 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]

bulb 說明:

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

  • 可以通過將迴圈變數作為命名變數傳遞給函式來獲得預期的結果. 為什麼這樣可行? 因為這會在函式內再次定義一個區域性變數.

    funcs = []
    for x in range(7):
        def some_func(x=x):
            return x
        funcs.append(some_func)

    Output:

    >>> funcs_results = [func() for func in funcs]
    >>> funcs_results
    [0, 1, 2, 3, 4, 5, 6]

> is not ... is not is (not ...)/is not ... 不是 is (not ...)

>>> 'something' is not None
True
>>> 'something' is (not None)
False

bulb 說明:

  • is not 是個單獨的二元運算子, 與分別使用 isnot 不同.
  • 如果操作符兩側的變數指向同一個物件, 則 is not 的結果為 False, 否則結果為 True.

> The surprising comma/意外的逗號

Output:

>>> def f(x, y,):
...     print(x, y)
...
>>> def g(x=4, y=5,):
...     print(x, y)
...
>>> def h(x, **kwargs,):
  File "<stdin>", line 1
    def h(x, **kwargs,):
                     ^
SyntaxError: invalid syntax
>>> def h(*args,):
  File "<stdin>", line 1
    def h(*args,):
                ^
SyntaxError: invalid syntax

bulb 說明:

  • 在Python函式的形式引數列表中, 尾隨逗號並不一定是合法的.
  • 在Python中, 引數列表部分用前置逗號定義, 部分用尾隨逗號定義. 這種衝突導致逗號被夾在中間, 沒有規則定義它.(譯:這一句看得我也很懵逼,只能強翻了.詳細解釋看下面的討論帖會一目瞭然.)
  • 注意: 尾隨逗號的問題已經在Python 3.6中被修復了. 而這篇帖子中則簡要討論了Python中尾隨逗號的不同用法.

> Backslashes at the end of string/字串末尾的反斜槓

Output:

>>> print("\\ C:\\")
\ C:\
>>> print(r"\ C:")
\ C:
>>> print(r"\ C:\")

    File "<stdin>", line 1
      print(r"\ C:\")
                     ^
SyntaxError: EOL while scanning string literal

bulb 說明:

  • 在以 r 開頭的原始字串中, 反斜槓並沒有特殊含義.
    >>> print(repr(r"wt\"f"))
    'wt\\"f'
  • 直譯器所做的只是簡單的改變了反斜槓的行為, 因此會直接放行反斜槓及後一個的字元. 這就是反斜槓在原始字串末尾不起作用的原因.

> not knot!/別糾結!

x = True
y = False

Output:

>>> not x == y
True
>>> x == not y
  File "<input>", line 1
    x == not y
           ^
SyntaxError: invalid syntax

bulb 說明:

  • 運算子的優先順序會影響表示式的求值順序, 而在 Python 中 == 運算子的優先順序要高於 not 運算子.
  • 所以 not x == y 相當於 not (x == y), 同時等價於 not (True == False), 最後的運算結果就是 True.
  • 之所以 x == not y 會拋一個 SyntaxError 異常, 是因為它會被認為等價於 (x == not) y, 而不是你一開始期望的 x == (not y).
  • 直譯器期望 not 標記是 not in 操作符的一部分 (因為 ==not in 操作符具有相同的優先順序), 但是它在 not 標記後面找不到 in 標記, 所以會丟擲 SyntaxError 異常.

> Half triple-quoted strings/三個引號

Output:

>>> print('wtfpython''')
wtfpython
>>> print("wtfpython""")
wtfpython
>>> # 下面的語句會丟擲 `SyntaxError` 異常
>>> # print('''wtfpython')
>>> # print("""wtfpython")

bulb 說明:

  • Python 提供隱式的字串連結, 例如,
    >>> print("wtf" "python")
    wtfpython
    >>> print("wtf" "") # or "wtf"""
    wtf
    
  • '''""" 在 Python中也是字串定界符, Python 直譯器在先遇到三個引號的的時候會嘗試再尋找三個終止引號作為定界符, 如果不存在則會導致 SyntaxError 異常.

> Midnight time doesn't exist?/不存在的午夜?

from datetime import datetime

midnight = datetime(2018, 1, 1, 0, 0)
midnight_time = midnight.time()

noon = datetime(2018, 1, 1, 12, 0)
noon_time = noon.time()

if midnight_time:
    print("Time at midnight is", midnight_time)

if noon_time:
    print("Time at noon is", noon_time)

Output:

('Time at noon is', datetime.time(12, 0))

midnight_time 並沒有被輸出.

bulb 說明:

在Python 3.5之前, 如果 datetime.time 物件儲存的UTC的午夜時間(譯: 就是 00:00), 那麼它的布林值會被認為是 False. 當使用 if obj: 語句來檢查 obj 是否為 null 或者某些“空”值的時候, 很容易出錯.


> What's wrong with booleans?/布林你咋了?

1.

# 一個簡單的例子, 統計下面可迭代物件中的布林型值的個數和整型值的個數
mixed_list = [False, 1.0, "some_string", 3, True, [], False]
integers_found_so_far = 0
booleans_found_so_far = 0

for item in mixed_list:
    if isinstance(item, int):
        integers_found_so_far += 1
    elif isinstance(item, bool):
        booleans_found_so_far += 1

Output:

>>> booleans_found_so_far
0
>>> integers_found_so_far
4

2.

another_dict = {}
another_dict[True] = "JavaScript"
another_dict[1] = "Ruby"
another_dict[1.0] = "Python"

Output:

>>> another_dict[True]
"Python"

3.

>>> some_bool = True
>>> "wtf"*some_bool
'wtf'
>>> some_bool = False
>>> "wtf"*some_bool
''

bulb 說明:

  • 布林值是 int 的子類

    >>> isinstance(True, int)
    True
    >>> isinstance(False, int)
    True
  • 所以 True 的整數值是 1, 而 False 的整數值是 0.

    >>> True == 1 == 1.0 and False == 0 == 0.0
    True
  • 關於其背後的原理, 請看這個 StackOverflow 的回答.


> Class attributes and instance attributes/類屬性和例項屬性

1.

class A:
    x = 1

class B(A):
    pass

class C(A):
    pass

Ouptut:

>>> A.x, B.x, C.x
(1, 1, 1)
>>> B.x = 2
>>> A.x, B.x, C.x
(1, 2, 1)
>>> A.x = 3
>>> A.x, B.x, C.x
(3, 2, 3)
>>> a = A()
>>> a.x, A.x
(3, 3)
>>> a.x += 1
>>> a.x, A.x
(4, 3)

2.

class SomeClass:
    some_var = 15
    some_list = [5]
    another_list = [5]
    def __init__(self, x):
        self.some_var = x + 1
        self.some_list = self.some_list + [x]
        self.another_list += [x]

Output:

>>> some_obj = SomeClass(420)
>>> some_obj.some_list
[5, 420]
>>> some_obj.another_list
[5, 420]
>>> another_obj = SomeClass(111)
>>> another_obj.some_list
[5, 111]
>>> another_obj.another_list
[5, 420, 111]
>>> another_obj.another_list is SomeClass.another_list
True
>>> another_obj.another_list is some_obj.another_list
True

bulb 說明:

  • 類變數和例項變數在內部是通過類物件的字典來處理(譯: 就是 __dict__ 屬性). 如果在當前類的字典中找不到的話就去它的父類中尋找.
  • += 運算子會在原地修改可變物件, 而不是建立新物件. 因此, 修改一個例項的屬性會影響其他例項和類屬性.

> yielding None/生成 None

some_iterable = ('a', 'b')

def some_func(val):
    return "something"

Output:

>>> [x for x in some_iterable]
['a', 'b']
>>> [(yield x) for x in some_iterable]
<generator object <listcomp> at 0x7f70b0a4ad58>
>>> list([(yield x) for x in some_iterable])
['a', 'b']
>>> list((yield x) for x in some_iterable)
['a', None, 'b', None]
>>> list(some_func((yield x)) for x in some_iterable)
['a', 'something', 'b', 'something']

bulb 說明:


> Mutating the immutable!/強人所難

some_tuple = ("A", "tuple", "with", "values")
another_tuple = ([1, 2], [3, 4], [5, 6])

Output:

>>> some_tuple[2] = "change this"
TypeError: 'tuple' object does not support item assignment
>>> another_tuple[2].append(1000) # 這裡不出現錯誤
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000])
>>> another_tuple[2] += [99, 999]
TypeError: 'tuple' object does not support item assignment
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000, 99, 999])

我還以為元組是不可變的呢...

bulb 說明:

  • 引用 https://docs.python.org/2/reference/datamodel.html

    不可變序列 不可變序列的物件一旦建立就不能再改變. (如果物件包含對其他物件的引用,則這些其他物件可能是可變的並且可能會被修改; 但是,由不可變物件直接引用的物件集合不能更改.)

  • += 操作符在原地修改了列表. 元素賦值操作並不工作, 但是當異常丟擲時, 元素已經在原地被修改了.

(譯: 對於不可變物件, 這裡指tuple, += 並不是原子操作, 而是 extend= 兩個動作, 這裡 = 操作雖然會丟擲異常, 但 extend 操作已經修改成功了. 詳細解釋可以看這裡)


> The disappearing variable from outer scope/消失的外部變數

e = 7
try:
    raise Exception()
except Exception as e:
    pass

Output (Python 2.x):

>>> print(e)
# prints nothing

Output (Python 3.x):

>>> print(e)
NameError: name 'e' is not defined

bulb 說明:

  • 出處: https://docs.python.org/3/reference/compound_stmts.html#except

    當使用 as 為目標分配異常的時候, 將在except子句的末尾清除該異常.

    這就好像

    except E as N:
        foo

    會被翻譯成

    except E as N:
        try:
            foo
        finally:
            del N

    這意味著異常必須在被賦值給其他變數才能在 except 子句之後引用它. 而異常之所以會被清除, 則是由於上面附加的回溯資訊(trackback)會和棧幀(stack frame)形成迴圈引用, 使得該棧幀中的所有本地變數在下一次垃圾回收發生之前都處於活動狀態.(譯: 也就是說不會被回收)

  • 子句在 Python 中並沒有獨立的作用域. 示例中的所有內容都處於同一作用域內, 所以變數 e 會由於執行了 except 子句而被刪除. 而對於有獨立的內部作用域的函式來說情況就不一樣了. 下面的例子說明了這一點:

    def f(x):
        del(x)
        print(x)
    
    x = 5
    y = [5, 4, 3]

    Output:

    >>>f(x)
    UnboundLocalError: local variable 'x' referenced before assignment
    >>>f(y)
    UnboundLocalError: local variable 'x' referenced before assignment
    >>> x
    5
    >>> y
    [5, 4, 3]
  • 在 Python 2.x 中, Exception() 例項被賦值給了變數 e, 所以當你嘗試列印結果的時候, 它的輸出為空.(譯: 正常的Exception例項打印出來就是空)

    Output (Python 2.x):

    >>> e
    Exception()
    >>> print e
    # 沒有列印任何內容!

> When True is actually False/真亦假

True = False
if True == False:
    print("I've lost faith in truth!")

Output:

I've lost faith in truth!

bulb 說明:

  • 最初, Python 並沒有 bool 型 (人們用0表示假值, 用非零值比如1作為真值). 後來他們添加了 True, False, 和 bool 型, 但是, 為了向後相容, 他們沒法把 TrueFalse 設定為常量, 只是設定成了內建變數.
  • Python 3 由於不再需要向後相容, 終於可以修復這個問題了, 所以這個例子無法在 Python 3.x 中執行!

> From filled to None in one instruction.../從有到無...

some_list = [1, 2, 3]
some_dict = {
  "key_1": 1,
  "key_2": 2,
  "key_3": 3
}

some_list = some_list.append(4)
some_dict = some_dict.update({"key_4": 4})

Output:

>>> print(some_list)
None
>>> print(some_dict)
None

bulb 說明:

大多數修改序列/對映物件的方法, 比如 list.append, dict.update, list.sort 等等. 都是原地修改物件並返回 None. 這樣做的理由是, 如果操作可以原地完成, 就可以避免建立物件的副本來提高效能. (參考這裡)


> Subclass relationships/子類關係 *

Output:

>>> from collections import Hashable
>>> issubclass(list, object)
True
>>> issubclass(object, Hashable)
True
>>> issubclass(list, Hashable)
False

子類關係應該是可傳遞的, 對吧? (即, 如果 AB 的子類, BC 的子類, 那麼 A 應該C 的子類.)

bulb 說明:

  • Python 中的子類關係並不必須是傳遞的. 任何人都可以在元類中隨意定義 __subclasscheck__.
  • issubclass(cls, Hashable) 被呼叫時, 它只是在 cls 中尋找 "__hash__" 方法或繼承自"__hash__"的方法.
  • 由於 object is 可雜湊的(hashable), 但是 list 是不可雜湊的, 所以它打破了這種傳遞關係.
  • 這裡可以找到更詳細的解釋.

> The mysterious key type conversion/神祕的鍵型轉換 *

class SomeClass(str):
    pass

some_dict = {'s':42}

Output:

>>> type(list(some_dict.keys())[0])
str
>>> s = SomeClass('s')
>>> some_dict[s] = 40
>>> some_dict # 預期: 兩個不同的鍵值對
{'s': 40}
>>> type(list(some_dict.keys())[0])
str

bulb 說明:

  • 由於 SomeClass 會從 str 自動繼承 __hash__ 方法, 所以 s 物件和 "s" 字串的雜湊值是相同的.

  • SomeClass("s") == "s"True 是因為 SomeClass 也繼承了 str__eq__ 方法.

  • 由於兩者的雜湊值相同且相等, 所以它們在字典中表示相同的鍵.

  • 如果想要實現期望的功能, 我們可以重定義 SomeClass__eq__ 方法.

    class SomeClass(str):
      def __eq__(self, other):
          return (
              type(self) is SomeClass
              and type(other) is SomeClass
              and super().__eq__(other)
          )
    
      # 當我們自定義 __eq__ 方法時, Python 不會再自動繼承 __hash__ 方法
      # 所以我們也需要定義它
      __hash__ = str.__hash__
    
    some_dict = {'s':42}

    Output:

    >>> s = SomeClass('s')
    >>> some_dict[s] = 40
    >>> some_dict
    {'s': 40, 's': 42}
    >>> keys = list(some_dict.keys())
    >>> type(keys[0]), type(keys[1])
    (__main__.SomeClass, str)

> Let's see if you can guess this?/看看你能否猜到這一點?

a, b = a[b] = {}, 5

Output:

>>> a
{5: ({...}, 5)}

bulb 說明:

  • 根據 Python 語言參考, 賦值語句的形式如下

    (target_list "=")+ (expression_list | yield_expression)
    

    賦值語句計算表示式列表(expression list)(牢記 這可以是單個表示式或以逗號分隔的列表, 後者返回元組)並將單個結果物件從左到右分配給目標列表中的每一項.

  • (target_list "=")+ 中的 + 意味著可以有一個或多個目標列表. 在這個例子中, 目標列表是 a, ba[b] (注意表示式列表只能有一個, 在我們的例子中是 {}, 5).

  • 表示式列表計算結束後, 將其值自動解包後從左到右分配給目標列表(target list). 因此, 在我們的例子中, 首先將 {}, 5 元祖並賦值給 a, b, 然後我們就可以得到 a = {}b = 5.

  • a 被賦值的 {} 是可變物件.

  • 第二個目標列表是 a[b] (你可能覺得這裡會報錯, 因為在之前的語句中 ab 都還沒有被定義. 但是別忘了, 我們剛剛將 a 賦值 {} 且將 b 賦值為 5).

  • 現在, 我們將通過將字典中鍵 5 的值設定為元祖 ({}, 5) 來建立迴圈引用 (輸出中的 {...} 指與 a 引用了相同的物件). 下面是一個更簡單的迴圈引用的例子

    >>> some_list = some_list[0] = [0]
    >>> some_list
    [[...]]
    >>> some_list[0]
    [[...]]
    >>> some_list is some_list[0]
    True
    >>> some_list[0][0][0][0][0][0] == some_list
    True

    我們的例子就是這種情況 (a[b][0]a 是相同的物件)

  • 總結一下, 你也可以把例子拆成

    a, b = {}, 5
    a[b] = a, b

    並且可以通過 a[b][0]a 是相同的物件來證明是迴圈引用

    >>> a[b][0] is a
    True


Section: Appearances are deceptive!/外表是靠不住的!

> Skipping lines?/跳過一行?

Output:

>>> value = 11
>>> valuе = 32
>>> value
11

什麼鬼?

注意: 如果你想要重現的話最簡單的方法是直接複製上面的程式碼片段到你的檔案或命令列裡.

bulb 說明:

一些非西方字元雖然看起來和英語字母相同, 但會被直譯器識別為不同的字母.

>>> ord('е') # 西里爾語的 'e' (Ye)
1077
>>> ord('e') # 拉丁語的 'e', 用於英文並使用標準鍵盤輸入
101
>>> 'е' == 'e'
False

>>> value = 42 # 拉丁語 e
>>> valuе = 23 # 西里爾語 'e', Python 2.x 的直譯器在這會丟擲 `SyntaxError` 異常
>>> value
42

內建的 ord() 函式可以返回一個字元的 Unicode 程式碼點, 這裡西里爾語 'e' 和拉丁語 'e' 的程式碼點不同證實了上述例子.


> Teleportation/空間移動 *

import numpy as np

def energy_send(x):
    # 初始化一個 numpy 陣列
    np.array([float(x)])

def energy_receive():
    # 返回一個空的 numpy 陣列
    return np.empty((), dtype=np.float).tolist()

Output:

>>> energy_send(123.456)
>>> energy_receive()
123.456

誰來給我發個諾貝爾獎?

bulb 說明:

  • 注意在 energy_send 函式中建立的 numpy 陣列並沒有返回, 因此記憶體空間被釋放並可以被重新分配.
  • numpy.empty() 直接返回下一段空閒記憶體,而不重新初始化. 而這個記憶體點恰好就是剛剛釋放的那個(通常情況下, 並不絕對).

> Well, something is fishy.../嗯,有些可疑...

def square(x):
    """
    一個通過加法計算平方的簡單函式.
    """
    sum_so_far = 0
    for counter in range(x):
        sum_so_far = sum_so_far + x
  return sum_so_far

Output (Python 2.x):

>>> square(10)
10

難道不應該是100嗎?

注意: 如果你無法重現, 可以嘗試執行這個檔案mixed_tabs_and_spaces.py.

bulb 說明:

  • 不要混用製表符(tab)和空格(space)! 在上面的例子中, return 的前面是"1個製表符", 而其他部分的程式碼前面是 "4個空格".

  • Python是這麼處理製表符的:

    首先, 製表符會從左到右依次被替換成8個空格, 直到被替換後的字元總數是八的倍數 <...>

  • 因此, square 函式最後一行的製表符會被替換成8個空格, 導致return語句進入迴圈語句裡面.

  • Python 3 很友好, 在這種情況下會自動丟擲錯誤.

    Output (Python 3.x):

    TabError: inconsistent use of tabs and spaces in indentation


Section: Watch out for the landmines!/小心地雷!

> Modifying a dictionary while iterating over it/迭代字典時的修改

x = {0: None}

for i in x:
    del x[i]
    x[i+1] = None
    print(i)

Output (Python 2.7- Python 3.5):

0
1
2
3
4
5
6
7

是的, 它運行了八次然後才停下來.

bulb 說明:

  • Python不支援對字典進行迭代的同時修改它.
  • 它之所以執行8次, 是因為字典會自