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

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

Python, 是一個設計優(yōu)美的解釋型高級語言, 它提供了很多能讓程序員感到舒適的功能特性. 但有的時候, Python 的一些輸出結果對于初學者來說似乎并不是那么一目了然.

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

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

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

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

?? 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.7 版本返回結果為 False.

3.

>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False # 3.7 版本返回結果為 True

很好理解, 對吧?

?? 說明:

  • 這些行為是由于 Cpython 在編譯優(yōu)化時, 某些情況下會嘗試使用已經存在的不可變對象而不是每次都創(chuàng)建一個新對象. (這種行為被稱作字符串的駐留[string interning])
  • 發(fā)生駐留之后, 許多變量可能指向內存中的相同字符串對象. (從而節(jié)省內存)
  • 在上面的代碼中, 字符串是隱式駐留的. 何時發(fā)生隱式駐留則取決于具體的實現. 這里有一些方法可以用來猜測字符串是否會被駐留:
    • 所有長度為 0 和長度為 1 的字符串都被駐留.
    • 字符串在編譯時被實現 ('wtf' 將被駐留, 但是 ''.join(['w', 't', 'f']) 將不會被駐留)
    • 字符串中只包含字母畔濒,數字或下劃線時將會駐留. 所以 'wtf!' 由于包含 ! 而未被駐留. 可以在這里找到 CPython 對此規(guī)則的實現.
  • 當在同一行將 ab 的值設置為 "wtf!" 的時候, Python 解釋器會創(chuàng)建一個新對象, 然后同時引用第二個變量(譯: 僅適用于3.7以下, 詳細情況請看這里). 如果你在不同的行上進行賦值操作, 它就不會“知道”已經有一個 wtf馍佑! 對象 (因為 "wtf!" 不是按照上面提到的方式被隱式駐留的). 它是一種編譯器優(yōu)化, 特別適用于交互式環(huán)境.
  • 常量折疊(constant folding) 是 Python 中的一種 窺孔優(yōu)化(peephole optimization) 技術. 這意味著在編譯時表達式 'a'*20 會被替換為 'aaaaaaaaaaaaaaaaaaaa' 以減少運行時的時鐘周期. 只有長度小于 20 的字符串才會發(fā)生常量折疊. (為啥? 想象一下由于表達式 'a'*10**10 而生成的 .pyc 文件的大小). 相關的源碼實現在這里.
  • 如果你是使用 3.7 版本中運行上述示例代碼, 會發(fā)現部分代碼的運行結果與注釋說明相同. 這是因為在 3.7 版本中, 常量折疊已經從窺孔優(yōu)化器遷移至新的 AST 優(yōu)化器, 后者可以以更高的一致性來執(zhí)行優(yōu)化. (由 Eugene Toder 和 INADA Naoki 在 bpo-29469bpo-11549 中貢獻.)
  • (譯: 但是在最新的 3.8 版本中, 結果又變回去了. 雖然 3.8 版本和 3.7 版本一樣, 都是使用 AST 優(yōu)化器. 目前不確定官方對 3.8 版本的 AST 做了什么調整.)

> 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
    
    注意: 具有不同值的對象也可能具有相同的哈希值(哈希沖突).
  • 當執(zhí)行 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'

?? 說明:

  • 當在 "try...finally" 語句的 try 中執(zhí)行 return, breakcontinue 后, finally 子句依然會執(zhí)行.
  • 函數的返回值由最后執(zhí)行的 return 語句決定. 由于 finally 子句一定會執(zhí)行, 所以 finally 子句中的 return 將始終是最后執(zhí)行的語句.

> 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

?? 說明:

  • 當調用 id 函數時, Python 創(chuàng)建了一個 WTF 類的對象并傳給 id 函數. 然后 id 函數獲取其id值 (也就是內存地址), 然后丟棄該對象. 該對象就被銷毀了.

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

  • 綜上, 對象的id值僅僅在對象的生命周期內唯一. 在對象被銷毀之后, 或被創(chuàng)建之前, 其他對象可以具有相同的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 # 創(chuàng)建了索引字典.
{0: 'w', 1: 't', 2: 'f'}

?? 說明:

  • Python 語法 中對 for 的定義是:

    for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
    
    其中 exprlist 指分配目標. 這意味著對可迭代對象中的每一項都會執(zhí)行類似 {exprlist} = {next_value} 的操作.

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

    for i in range(4):
        print(i)
        i = 10
    

    Output:

    0
    1
    2
    3
    

    你可曾覺得這個循環(huán)只會運行一次?

    ?? 說明:

  • 由于循環(huán)在Python中工作方式, 賦值語句 i = 10 并不會影響迭代循環(huán), 在每次迭代開始之前, 迭代器(這里指 range(4)) 生成的下一個元素就被解包并賦值給目標列表的變量(這里指 i)了.

  • 在每一次的迭代中, enumerate(some_string) 函數就生成一個新值 i (計數器增加) 并從 some_string 中獲取一個字符. 然后將字典 some_dicti (剛剛分配的) 的值設為該字符. 本例中循環(huán)的展開可以簡化為:

    >>> i, some_dict[i] = (0, 'w')
    >>> i, some_dict[i] = (1, 't')
    >>> i, some_dict[i] = (2, 'f')
    >>> some_dict
    

> Evaluation time discrepancy/執(zhí)行時機差異

1.

array = [1, 8, 15]
g = (x for x in array if array.count(x) > 0)
# 返回 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]

?? 說明

  • 生成器表達式中, in 子句在聲明時執(zhí)行, 而條件子句則是在運行時執(zhí)行.
  • 所以在運行前, array 已經被重新賦值為 [2, 8, 22], 因此對于之前的 1, 815, 只有 count(8) 的結果是大于 0 的, 所以生成器只會生成 8.
  • 第二部分中 g1g2 的輸出差異則是由于變量 array_1array_2 被重新賦值的方式導致的.
  • 在第一種情況下, array_1 被綁定到新對象 [1,2,3,4,5], 因為 in 子句是在聲明時被執(zhí)行的恬总, 所以它仍然引用舊對象 [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

?? 說明:

is== 的區(qū)別

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

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

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

Python 通過這種創(chuàng)建小整數池的方式來避免小整數頻繁的申請和銷毀內存空間.引用自

當前的實現為-5到256之間的所有整數保留一個整數對象數組, 當你創(chuàng)建了一個該范圍內的整數時, 你只需要返回現有對象的引用. 所以改變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

這里解釋器并沒有智能到能在執(zhí)行 y = 257 時意識到我們已經創(chuàng)建了一個整數 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 解釋器會創(chuàng)建一個新對象, 然后同時引用第二個變量. 如果你在不同的行上進行, 它就不會 "知道" 已經存在一個 257 對象了.
  • 這是一種特別為交互式環(huán)境做的編譯器優(yōu)化. 當你在實時解釋器中輸入兩行的時候, 他們會單獨編譯, 因此也會單獨進行優(yōu)化. 如果你在 .py 文件中嘗試這個例子, 則不會看到相同的行為, 因為文件是一次性編譯的.

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

# 我們先初始化一個變量row
row = [""]*3 #row i['', '', '']
# 并創(chuàng)建一個變量board
board = [row]*3

Output:

>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]

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

?? 說明:

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

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

我們可以通過不使用變量 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()) # 注意這里函數被執(zhí)行了

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]

?? 說明:

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

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

    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

?? 說明:

  • 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

?? 說明:

  • 在Python函數的形式參數列表中, 尾隨逗號并不一定是合法的.
  • 在Python中, 參數列表部分用前置逗號定義, 部分用尾隨逗號定義. 這種沖突導致逗號被夾在中間, 沒有規(guī)則定義它.(譯:這一句看得我也很懵逼,只能強翻了.詳細解釋看下面的討論帖會一目了然.)
  • 注意:尾隨逗號的問題已經在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

?? 說明:

  • 在以 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

?? 說明:

  • 運算符的優(yōu)先級會影響表達式的求值順序, 而在 Python 中 == 運算符的優(yōu)先級要高于 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 操作符具有相同的優(yōu)先級), 但是它在 not 標記后面找不到 in 標記, 所以會拋出 SyntaxError 異常.

> Half triple-quoted strings/三個引號

Output:

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

?? 說明:

  • 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 并沒有被輸出.

?? 說明:

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


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

1.

# 一個簡單的例子, 統(tǒng)計下面可迭代對象中的布爾型值的個數和整型值的個數
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:

>>> integers_found_so_far
4
>>> booleans_found_so_far
0

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

?? 說明:

  • 布爾值是 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

Output:

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

?? 說明:

  • 類變量和實例變量在內部是通過類對象的字典來處理(譯: 就是 __dict__ 屬性). 如果在當前類的字典中找不到的話就去它的父類中尋找.
  • += 運算符會在原地修改可變對象, 而不是創(chuàng)建新對象. 因此, 在這種情況下, 修改一個實例的屬性會影響其他實例和類屬性.

> 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']

?? 說明:


> 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])

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

?? 說明:

  • 引用

    不可變序列
    不可變序列的對象一旦創(chuàng)建就不能再改變. (如果對象包含對其他對象的引用,則這些其他對象可能是可變的并且可能會被修改; 但是奸远,由不可變對象直接引用的對象集合不能更改.)

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

(譯: 對于不可變對象, 這里指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

?? 說明:

  • 出處

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

    這就好像

    except E as N:
        foo
    

    會被翻譯成

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

    這意味著異常必須在被賦值給其他變量才能在 except 子句之后引用它. 而異常之所以會被清除, 則是由于上面附加的回溯信息(trackback)會和棧幀(stack frame)形成循環(huán)引用, 使得該棧幀中的所有本地變量在下一次垃圾回收發(fā)生之前都處于活動狀態(tài).(譯: 也就是說不會被回收)

  • 子句在 Python 中并沒有獨立的作用域. 示例中的所有內容都處于同一作用域內, 所以變量 e 會由于執(zhí)行了 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!

?? 說明:

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

> 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

?? 說明:

大多數修改序列/映射對象的方法, 比如 list.append, dict.update, list.sort 等等. 都是原地修改對象并返回 None. 這樣做的理由是, 如果操作可以原地完成, 就可以避免創(chuàng)建對象的副本來提高性能. (參考這里)


> Subclass relationships/子類關系 *

Output:

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

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

?? 說明:

  • 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

?? 說明:

  • 由于 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)}

?? 說明:

  • 根據 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) 來創(chuàng)建循環(huán)引用 (輸出中的 {...} 指與 a 引用了相同的對象). 下面是一個更簡單的循環(huán)引用的例子

    >>> 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 是相同的對象來證明是循環(huán)引用

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


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

> Skipping lines?/跳過一行?

Output:

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

什么鬼?

注意:如果你想要重現的話最簡單的方法是直接復制上面的代碼片段到你的文件或命令行里.

?? 說明:

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

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

誰來給我發(fā)個諾貝爾獎?

?? 說明:

  • 注意在 energy_send 函數中創(chuàng)建的 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.

?? 說明:

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

  • Python是這么處理制表符的:

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

  • 因此, square 函數最后一行的制表符會被替換成8個空格, 導致return語句進入循環(huán)語句里面.

  • 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

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

?? 說明:

  • Python不支持對字典進行迭代的同時修改它.
  • 它之所以運行8次, 是因為字典會自動擴容以容納更多鍵值(我們有8次刪除記錄, 因此需要擴容). 這實際上是一個實現細節(jié). (譯: 應該是因為字典的初始最小值是8, 擴容會導致散列表地址發(fā)生變化而中斷循環(huán).)
  • 在不同的Python實現中刪除鍵的處理方式以及調整大小的時間可能會有所不同.(譯: 就是說什么時候擴容在不同版本中可能是不同的, 在3.6及3.7的版本中到5就會自動擴容了. 以后也有可能再次發(fā)生變化. 這是為了避免散列沖突. 順帶一提, 后面兩次擴容會擴展為32和256. 即8->32->256.)
  • 更多的信息, 你可以參考這個StackOverflow的回答, 它詳細的解釋一個類似的例子.

> Stubborn del operator/堅強的 del

class SomeClass:
    def __del__(self):
        print("Deleted!")

Output:
1.

>>> x = SomeClass()
>>> y = x
>>> del x # 這里應該會輸出 "Deleted!"
>>> del y
Deleted!

唷, 終于刪除了. 你可能已經猜到了在我們第一次嘗試刪除 x 時是什么讓 __del__ 免于被調用的. 那讓我們給這個例子增加點難度.

2.

>>> x = SomeClass()
>>> y = x
>>> del x
>>> y # 檢查一下y是否存在
<__main__.SomeClass instance at 0x7f98a1a67fc8>
>>> del y # 像之前一樣, 這里應該會輸出 "Deleted!"
>>> globals() # 好吧, 并沒有. 讓我們看一下所有的全局變量
Deleted!
{'__builtins__': <module '__builtin__' (built-in)>, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, '__package__': None, '__name__': '__main__', '__doc__': None}

好了懒叛,現在它被刪除了 :confused:

?? 說明:

  • del x 并不會立刻調用 x.__del__().
  • 每當遇到 del x, Python 會將 x 的引用數減1, 當 x 的引用數減到0時就會調用 x.__del__().
  • 在第二個例子中, y.__del__() 之所以未被調用, 是因為前一條語句 (>>> y) 對同一對象創(chuàng)建了另一個引用, 從而防止在執(zhí)行 del y 后對象的引用數變?yōu)?.
  • **調用 globals 導致引用被銷毀, 因此我們可以看到 "Deleted!" 終于被輸出了.
  • (譯: 這其實是 Python 交互解釋器的特性, 它會自動讓 _ 保存上一個表達式輸出的值, 詳細可以看這里.)**

> Deleting a list item while iterating/迭代列表時刪除元素

list_1 = [1, 2, 3, 4]
list_2 = [1, 2, 3, 4]
list_3 = [1, 2, 3, 4]
list_4 = [1, 2, 3, 4]

for idx, item in enumerate(list_1):
    del item

for idx, item in enumerate(list_2):
    list_2.remove(item)

for idx, item in enumerate(list_3[:]):
    list_3.remove(item)

for idx, item in enumerate(list_4):
    list_4.pop(idx)

Output:

>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]

你能猜到為什么輸出是 [2, 4] 嗎?

?? 說明:

  • 在迭代時修改對象是一個很愚蠢的主意. 正確的做法是迭代對象的副本, list_3[:] 就是這么做的.

    >>> some_list = [1, 2, 3, 4]
    >>> id(some_list)
    139798789457608
    >>> id(some_list[:]) # 注意python為切片列表創(chuàng)建了新對象.
    139798779601192
    

del, removepop 的不同:

  • del var_name 只是從本地或全局命名空間中刪除了 var_name (這就是為什么 list_1 沒有受到影響).
  • remove 會刪除第一個匹配到的指定值, 而不是特定的索引, 如果找不到值則拋出 ValueError 異常.
  • pop 則會刪除指定索引處的元素并返回它, 如果指定了無效的索引則拋出 IndexError 異常.

為什么輸出是 [2, 4]?

  • 列表迭代是按索引進行的, 所以當我們從 list_2list_4 中刪除 1 時, 列表的內容就變成了 [2, 3, 4]. 剩余元素會依次位移, 也就是說, 2 的索引會變?yōu)?0, 3 會變?yōu)?1. 由于下一次迭代將獲取索引為 1 的元素 (即 3), 因此 2 將被徹底的跳過. 類似的情況會交替發(fā)生在列表中的每個元素上.

  • 參考這個StackOverflow的回答來解釋這個例子

  • 關于Python中字典的類似例子, 可以參考這個Stackoverflow的回答.


> Loop variables leaking out!/循環(huán)變量泄漏!

1.

for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

Output:

6 : for x inside loop
6 : x in global

但是 x 從未在循環(huán)外被定義...

2.

# 這次我們先初始化x
x = -1
for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

Output:

6 : for x inside loop
6 : x in global

3.

x = 1
print([x for x in range(5)])
print(x, ': x in global')

Output (on Python 2.x):

[0, 1, 2, 3, 4]
(4, ': x in global')

Output (on Python 3.x):

[0, 1, 2, 3, 4]
1 : x in global

?? 說明:

  • 在 Python 中, for 循環(huán)使用所在作用域并在結束后保留定義的循環(huán)變量. 如果我們曾在全局命名空間中定義過循環(huán)變量. 在這種情況下, 它會重新綁定現有變量.

  • Python 2.x 和 Python 3.x 解釋器在列表推導式示例中的輸出差異, 在文檔 What’s New In Python 3.0 中可以找到相關的解釋:

    "列表推導不再支持句法形式 [... for var in item1, item2, ...]. 取而代之的是 [... for var in (item1, item2, ...)]. 另外, 注意列表推導具有不同的語義: 它們更接近于 list() 構造函數中生成器表達式的語法糖(譯: 這一句我也不是很明白), 特別是循環(huán)控制變量不再泄漏到周圍的作用域中."


> Beware of default mutable arguments!/當心默認的可變參數!

def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg

Output:

>>> some_func()
['some_string']
>>> some_func()
['some_string', 'some_string']
>>> some_func([])
['some_string']
>>> some_func()
['some_string', 'some_string', 'some_string']

?? 說明:

  • Python中函數的默認可變參數并不是每次調用該函數時都會被初始化. 相反, 它們會使用最近分配的值作為默認值. 當我們明確的將 [] 作為參數傳遞給 some_func 的時候, 就不會使用 default_arg 的默認值, 所以函數會返回我們所期望的結果.

    def some_func(default_arg=[]):
        default_arg.append("some_string")
        return default_arg
    

    Output:

    >>> some_func.__defaults__ # 這里會顯示函數的默認參數的值
    ([],)
    >>> some_func()
    >>> some_func.__defaults__
    (['some_string'],)
    >>> some_func()
    >>> some_func.__defaults__
    (['some_string', 'some_string'],)
    >>> some_func([])
    >>> some_func.__defaults__
    (['some_string', 'some_string'],)
    
  • 避免可變參數導致的錯誤的常見做法是將 None 指定為參數的默認值, 然后檢查是否有值傳給對應的參數. 例:

    def some_func(default_arg=None):
        if not default_arg:
            default_arg = []
        default_arg.append("some_string")
        return default_arg
    

> Catching the Exceptions/捕獲異常

some_list = [1, 2, 3]
try:
    # 這里會拋出異常 ``IndexError``
    print(some_list[4])
except IndexError, ValueError:
    print("Caught!")

try:
    # 這里會拋出異常 ``ValueError``
    some_list.remove(4)
except IndexError, ValueError:
    print("Caught again!")

Output (Python 2.x):

Caught!

ValueError: list.remove(x): x not in list

Output (Python 3.x):

  File "<input>", line 3
    except IndexError, ValueError:
                     ^
SyntaxError: invalid syntax

?? 說明:

  • 如果你想要同時捕獲多個不同類型的異常時, 你需要將它們用括號包成一個元組作為第一個參數傳遞. 第二個參數是可選名稱, 如果你提供, 它將與被捕獲的異常實例綁定. 例,

    some_list = [1, 2, 3]
    try:
       # 這里會拋出異常 ``ValueError``
       some_list.remove(4)
    except (IndexError, ValueError), e:
       print("Caught again!")
       print(e)
    

    Output (Python 2.x):

    Caught again!
    list.remove(x): x not in list
    

    Output (Python 3.x):

      File "<input>", line 4
        except (IndexError, ValueError), e:
                                         ^
    IndentationError: unindent does not match any outer indentation level
    
  • 在 Python 3 中, 用逗號區(qū)分異常與可選名稱是無效的; 正確的做法是使用 as 關鍵字. 例,

    some_list = [1, 2, 3]
    try:
        some_list.remove(4)
    
    except (IndexError, ValueError) as e:
        print("Caught again!")
        print(e)
    

    Output:

    Caught again!
    list.remove(x): x not in list
    

> Same operands, different story!/同人不同命!

1.

a = [1, 2, 3, 4]
b = a
a = a + [5, 6, 7, 8]

Output:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4]

2.

a = [1, 2, 3, 4]
b = a
a += [5, 6, 7, 8]

Output:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8]

?? 說明:

  • a += b 并不總是與 a = a + b 表現相同. 類實現 op= 運算符的方式 也許 是不同的, 列表就是這樣做的.

  • 表達式 a = a + [5,6,7,8] 會生成一個新列表, 并讓 a 引用這個新列表, 同時保持 b 不變.

  • 表達式 a += [5,6,7,8] 實際上是使用的是 "extend" 函數, 所以 ab 仍然指向已被修改的同一列表.


> The out of scope variable/外部作用域變量

a = 1
def some_func():
    return a

def another_func():
    a += 1
    return a

Output:

>>> some_func()
1
>>> another_func()
UnboundLocalError: local variable 'a' referenced before assignment

?? 說明:

  • 當你在作用域中對變量進行賦值時, 變量會變成該作用域內的局部變量. 因此 a 會變成 another_func 函數作用域中的局部變量, 但它在函數作用域中并沒有被初始化, 所以會引發(fā)錯誤.

  • 可以閱讀這個簡短卻很棒的指南, 了解更多關于 Python 中命名空間和作用域的工作原理.

  • 想要在 another_func 中修改外部作用域變量 a 的話, 可以使用 global 關鍵字.

    def another_func()
        global a
        a += 1
        return a
    

    Output:

    >>> another_func()
    2
    

> Be careful with chained operations/小心鏈式操作

>>> (False == False) in [False] # 可以理解
False
>>> False == (False in [False]) # 可以理解
False
>>> False == False in [False] # 為毛?
True

>>> True is False == False
False
>>> False is False is False
True

>>> 1 > 0 < 1
True
>>> (1 > 0) < 1
False
>>> 1 > (0 < 1)
False

?? 說明:

根據

形式上, 如果 a, b, c, ..., y, z 是表達式, 而 op1, op2, ..., opN 是比較運算符, 那么除了每個表達式最多只出現一次以外 a op1 b op2 c ... y opN z 就等于 a op1 b and b op2 c and ... y opN z.

雖然上面的例子似乎很愚蠢, 但是像 a == b == c0 <= x <= 100 就很棒了.

  • False is False is False 相當于 (False is False) and (False is False)
  • True is False == False 相當于 True is False and False == False, 由于語句的第一部分 (True is False) 等于 False, 因此整個表達式的結果為 False.
  • 1 > 0 < 1 相當于 1 > 0 and 0 < 1, 所以最終結果為 True.
  • 表達式 (1 > 0) < 1 相當于 True < 1
    >>> int(True)
    1
    >>> True + 1 # 與這個例子無關,只是好玩
    2
    
    所以, 1 < 1 等于 False

> Name resolution ignoring class scope/忽略類作用域的名稱解析

1.

x = 5
class SomeClass:
    x = 17
    y = (x for i in range(10))

Output:

>>> list(SomeClass.y)[0]
5

2.

x = 5
class SomeClass:
    x = 17
    y = [x for i in range(10)]

Output (Python 2.x):

>>> SomeClass.y[0]
17

Output (Python 3.x):

>>> SomeClass.y[0]
5

?? 說明:

  • 類定義中嵌套的作用域會忽略類內的名稱綁定.
  • 生成器表達式有它自己的作用域.
  • 從 Python 3.X 開始, 列表推導式也有自己的作用域.

> Needle in a Haystack/大海撈針

1.

x, y = (0, 1) if True else None, None

Output:

>>> x, y  # 期望的結果是 (0, 1)
((0, 1), None)

幾乎每個 Python 程序員都遇到過類似的情況.

2.

t = ('one', 'two')
for i in t:
    print(i)

t = ('one')
for i in t:
    print(i)

t = ()
print(t)

Output:

one
two
o
n
e
tuple()

?? 說明:

  • 對于 1, 正確的語句是 x, y = (0, 1) if True else (None, None).
  • 對于 2, 正確的語句是 t = ('one',) 或者 t = 'one', (缺少逗號) 否則解釋器會認為 t 是一個字符串, 并逐個字符對其進行迭代.
  • () 是一個特殊的標記耽梅,表示空元組.


Section: The Hidden treasures!/隱藏的寶藏!

This section contains few of the lesser-known interesting things about Python that most beginners like me are unaware of (well, not anymore).

> Okay Python, Can you make me fly?/Python, 可否帶我飛? *

好, 去吧.

import antigravity

Output:
噓.. 這是個超級秘密.

?? 說明:

  • **antigravity 模塊是 Python 開發(fā)人員發(fā)布的少數復活節(jié)彩蛋之一.
  • import antigravity 會打開一個 Python 的經典 XKCD 漫畫頁面.**
  • 不止如此. 這個復活節(jié)彩蛋里還有一個復活節(jié)彩蛋. 如果你看一下代碼, 就會發(fā)現還有一個函數實現了 XKCD's geohashing 算法.

> goto, but why?/goto, 但為什么? *

from goto import goto, label
for i in range(9):
    for j in range(9):
        for k in range(9):
            print("I'm trapped, please rescue!")
            if k == 2:
                goto .breakout # 從多重循環(huán)中跳出
label .breakout
print("Freedom!")

Output (Python 2.3):

I'm trapped, please rescue!
I'm trapped, please rescue!
Freedom!

?? 說明:

  • 2004年4月1日, Python 宣布 加入一個可用的 goto 作為愚人節(jié)禮物.
  • 當前版本的 Python 并沒有這個模塊.
  • 就算可以用, 也請不要使用它. 這里是為什么Python中沒有 goto原因.

> Brace yourself!/做好思想準備 *

如果你不喜歡在Python中使用空格來表示作用域, 你可以導入 C 風格的 {},

from __future__ import braces

Output:

  File "some_file.py", line 1
    from __future__ import braces
SyntaxError: not a chance

想用大括號? 沒門! 覺得不爽, 請去用java.

?? 說明:

  • 通常 __future__ 會提供 Python 未來版本的功能. 然而芍瑞,這里的 “未來” 是一個諷刺.
  • 這是一個表達社區(qū)對此類問題態(tài)度的復活節(jié)彩蛋.

> Let's meet Friendly Language Uncle For Life/讓生活更友好 *

Output (Python 3.x)

>>> from __future__ import barry_as_FLUFL
>>> "Ruby" != "Python" # 這里沒什么疑問
  File "some_file.py", line 1
    "Ruby" != "Python"
              ^
SyntaxError: invalid syntax

>>> "Ruby" <> "Python"
True

這就對了.

?? 說明:

  • 相關的 PEP-401 發(fā)布于 2009年4月1日 (所以你現在知道這意味著什么了吧).

  • 引用 PEP-401

    意識到 Python 3.0 里的 != 運算符是一個會引起手指疼痛的恐怖錯誤, FLUFL 將 <> 運算符恢復為唯一寫法.

  • Uncle Barry 在 PEP 中還分享了其他東西; 你可以在這里獲得他們.

  • (譯: 雖然文檔中沒寫,但應該是只能在交互解釋器中使用.)


> Even Python understands that love is complicated/連Python也知道愛是難言的

import this

等等, this 是什么? this 是愛 :heart:

Output:

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
優(yōu)美勝于丑陋(Python 以編寫優(yōu)美的代碼為目標)
Explicit is better than implicit.
明了勝于晦澀(優(yōu)美的代碼應當是明了的褐墅,命名規(guī)范,風格相似)
Simple is better than complex.
簡潔勝于復雜(優(yōu)美的代碼應當是簡潔的洪己,不要有復雜的內部實現)
Complex is better than complicated.
復雜勝于凌亂(如果復雜不可避免妥凳,那代碼間也不能有難懂的關系,要保持接口簡潔)
Flat is better than nested.
扁平勝于嵌套(優(yōu)美的代碼應當是扁平的答捕,不能有太多的嵌套)
Sparse is better than dense.
間隔勝于緊湊(優(yōu)美的代碼有適當的間隔逝钥,不要奢望一行代碼解決問題)
Readability counts.
可讀性很重要(優(yōu)美的代碼一定是可讀的)
Special cases aren't special enough to break the rules.
沒有特例特殊到需要違背這些規(guī)則(這些規(guī)則至高無上)
Although practicality beats purity.
盡管我們更傾向于實用性
Errors should never pass silently.
不要安靜的包容所有錯誤
Unless explicitly silenced.
除非你確定需要這樣做(精準地捕獲異常,不寫 except:pass 風格的代碼)
In the face of ambiguity, refuse the temptation to guess.
拒絕誘惑你去猜測的曖昧事物
There should be one-- and preferably only one --obvious way to do it.
而是盡量找一種,最好是唯一一種明顯的解決方案(如果不確定艘款,就用窮舉法)
Although that way may not be obvious at first unless you're Dutch.
雖然這并不容易持际,因為你不是 Python 之父(這里的 Dutch 是指 Guido )
Now is better than never.
現在行動好過永遠不行動
Although never is often better than *right* now.
盡管不行動要好過魯莽行動
If the implementation is hard to explain, it's a bad idea.
如果你無法向人描述你的方案,那肯定不是一個好方案哗咆;
If the implementation is easy to explain, it may be a good idea.
如果你能輕松向人描述你的方案蜘欲,那也許會是一個好方案(方案測評標準)
Namespaces are one honking great idea -- let's do more of those!
命名空間是一種絕妙的理念,我們應當多加利用(倡導與號召)

這是 Python 之禪!

>>> love = this
>>> this is love
True
>>> love is True
False
>>> love is False
False
>>> love is not True or False
True
>>> love is not True or False; love is love  # 愛是難言的
True

?? 說明:

  • this 模塊是關于 Python 之禪的復活節(jié)彩蛋 (PEP 20).
  • 如果你認為這已經夠有趣的了, 可以看看 this.py 的實現. 有趣的是, Python 之禪的實現代碼違反了他自己 (這可能是唯一會發(fā)生這種情況的地方).
  • 至于 love is not True or False; love is love, 意外卻又不言而喻.

> Yes, it exists!/是的, 它存在!

循環(huán)的 else.一個典型的例子:

  def does_exists_num(l, to_find):
      for num in l:
          if num == to_find:
              print("Exists!")
              break
      else:
          print("Does not exist")

Output:

>>> some_list = [1, 2, 3, 4, 5]
>>> does_exists_num(some_list, 4)
Exists!
>>> does_exists_num(some_list, -1)
Does not exist

異常的 else .例,

try:
    pass
except:
    print("Exception occurred!!!")
else:
    print("Try block executed successfully...")

Output:

Try block executed successfully...

?? 說明:

  • 循環(huán)后的 else 子句只會在循環(huán)沒有觸發(fā) break 語句, 正常結束的情況下才會執(zhí)行.
  • try 之后的 else 子句也被稱為 "完成子句", 因為在 try 語句中到達 else 子句意味著try塊實際上已成功完成.

> Inpinity/無限

英文拼寫是有意的, 請不要為此提交補丁.
(譯: 這里是為了突出 Python 中無限的定義與Pi有關, 所以將兩個單詞拼接了.)

Output (Python 3.x):

>>> infinity = float('infinity')
>>> hash(infinity)
314159
>>> hash(float('-inf'))
-314159

?? 說明:

  • infinity 的哈希值是 10? x π.
  • 有意思的是, float('-inf') 的哈希值在 Python 3 中是 "-10? x π" , 而在 Python 2 中是 "-10? x e".

> Mangling time!/修飾時間! *

class Yo(object):
    def __init__(self):
        self.__honey = True
        self.bitch = True

Output:

>>> Yo().bitch
True
>>> Yo().__honey
AttributeError: 'Yo' object has no attribute '__honey'
>>> Yo()._Yo__honey
True

為什么 Yo()._Yo__honey 能運行? 只有印度人理解.(譯: 這個股渭恚可能是指印度音樂人Yo Yo Honey Singh)

?? 說明:

  • 名字修飾 用于避免不同命名空間之間名稱沖突.
  • 在 Python 中, 解釋器會通過給類中以 __ (雙下劃線)開頭且結尾最多只有一個下劃線的類成員名稱加上_NameOfTheClass 來修飾(mangles)名稱.
  • 所以, 要訪問 __honey 對象,我們需要加上 _Yo 以防止與其他類中定義的相同名稱的屬性發(fā)生沖突.


Section: Miscellaneous/雜項

> += is faster/更快的 +=

# 用 "+" 連接三個字符串:
>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.25748300552368164
# 用 "+=" 連接三個字符串:
>>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.012188911437988281

?? 說明:

  • 連接兩個以上的字符串時 +=+ 更快, 因為在計算過程中第一個字符串 (例如, s1 += s2 + s3 中的 s1) 不會被銷毀.(譯: 就是 += 執(zhí)行的是追加操作姥份,少了一個銷毀新建的動作.)

> Let's make a giant string!/來做個巨大的字符串吧!

def add_string_with_plus(iters):
    s = ""
    for i in range(iters):
        s += "xyz"
    assert len(s) == 3*iters

def add_bytes_with_plus(iters):
    s = b""
    for i in range(iters):
        s += b"xyz"
    assert len(s) == 3*iters

def add_string_with_format(iters):
    fs = "{}"*iters
    s = fs.format(*(["xyz"]*iters))
    assert len(s) == 3*iters

def add_string_with_join(iters):
    l = []
    for i in range(iters):
        l.append("xyz")
    s = "".join(l)
    assert len(s) == 3*iters

def convert_list_to_string(l, iters):
    s = "".join(l)
    assert len(s) == 3*iters

Output:

>>> timeit(add_string_with_plus(10000))
1000 loops, best of 3: 972 μs per loop
>>> timeit(add_bytes_with_plus(10000))
1000 loops, best of 3: 815 μs per loop
>>> timeit(add_string_with_format(10000))
1000 loops, best of 3: 508 μs per loop
>>> timeit(add_string_with_join(10000))
1000 loops, best of 3: 878 μs per loop
>>> l = ["xyz"]*10000
>>> timeit(convert_list_to_string(l, 10000))
10000 loops, best of 3: 80 μs per loop

讓我們將迭代次數增加10倍.

>>> timeit(add_string_with_plus(100000)) # 執(zhí)行時間線性增加
100 loops, best of 3: 9.75 ms per loop
>>> timeit(add_bytes_with_plus(100000)) # 二次增加
1000 loops, best of 3: 974 ms per loop
>>> timeit(add_string_with_format(100000)) # 線性增加
100 loops, best of 3: 5.25 ms per loop
>>> timeit(add_string_with_join(100000)) # 線性增加
100 loops, best of 3: 9.85 ms per loop
>>> l = ["xyz"]*100000
>>> timeit(convert_list_to_string(l, 100000)) # 線性增加
1000 loops, best of 3: 723 μs per loop

?? 說明:

  • 你可以在這獲得更多 timeit 的相關信息. 它通常用于衡量代碼片段的執(zhí)行時間.
  • 不要用 + 去生成過長的字符串, 在 Python 中, str 是不可變得, 所以在每次連接中你都要把左右兩個字符串復制到新的字符串中. 如果你連接四個長度為10的字符串, 你需要拷貝 (10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 個字符而不是 40 個字符. 隨著字符串的數量和大小的增加, 情況會變得越發(fā)的糟糕 (就像add_bytes_with_plus 函數的執(zhí)行時間一樣)
  • 因此, 更建議使用 .format.% 語法 (但是, 對于短字符串, 它們比 + 稍慢一點).
  • 又或者, 如果你所需的內容已經以可迭代對象的形式提供了, 使用 ''.join(可迭代對象) 要快多了.
  • add_string_with_plus 的執(zhí)行時間沒有像 add_bytes_with_plus 一樣出現二次增加是因為解釋器會如同上一個列子所討論的一樣優(yōu)化 +=. 用 s = s + "x" + "y" + "z" 替代 s += "xyz" 的話, 執(zhí)行時間就會二次增加了.
    def add_string_with_plus(iters):
        s = ""
        for i in range(iters):
            s = s + "x" + "y" + "z"
        assert len(s) == 3*iters
    
    >>> timeit(add_string_with_plus(10000))
    100 loops, best of 3: 9.87 ms per loop
    >>> timeit(add_string_with_plus(100000)) # 執(zhí)行時間二次增加
    1 loops, best of 3: 1.09 s per loop
    

> Explicit typecast of strings/字符串的顯式類型轉換

a = float('inf')
b = float('nan')
c = float('-iNf')  # 這些字符串不區(qū)分大小寫
d = float('nan')

Output:

>>> a
inf
>>> b
nan
>>> c
-inf
>>> float('some_other_string')
ValueError: could not convert string to float: some_other_string
>>> a == -c #inf==inf
True
>>> None == None # None==None
True
>>> b == d #但是 nan!=nan
False
>>> 50/a
0.0
>>> a/a
nan
>>> 23 + b
nan

?? 說明:

'inf''nan' 是特殊的字符串(不區(qū)分大小寫), 當顯示轉換成 float 型時, 它們分別用于表示數學意義上的 "無窮大" 和 "非數字".


> Minor Ones/小知識點

  • join() 是一個字符串操作而不是列表操作. (第一次接觸會覺得有點違反直覺)

    ?? 說明:
    如果 join() 是字符串方法 那么它就可以處理任何可迭代的對象(列表年碘,元組澈歉,迭代器). 如果它是列表方法, 則必須在每種類型中單獨實現. 另外, 在 list 對象的通用API中實現一個專用于字符串的方法沒有太大的意義.

  • 看著奇怪但能正確運行的語句:

    • [] = () 語句在語義上是正確的 (解包一個空的 tuple 并賦值給 list)
    • 'a'[0][0][0][0][0] 在語義上也是正確的, 因為在 Python 中字符串同時也是序列(可迭代對象支持使用整數索引訪問元素).
    • 3 --0-- 5 == 8--5 == 5 在語義上都是正確的, 且結果等于 True.(譯: 3減負0等于3,再減負5相當于加5等于8屿衅;負的負5等于5.)
  • 鑒于 a 是一個數組, ++a--a 都是有效的 Python 語句, 但其效果與 C, C++ 或 Java 等不一樣.

    >>> a = 5
    >>> a
    5
    >>> ++a
    5
    >>> --a
    5
    

    ?? 說明:

    • python 里沒有 ++ 操作符. 這其實是兩個 + 操作符.
    • ++a 被解析為 +(+a) 最后等于 a. --a 同理.
    • 這個 StackOverflow 回答 討論了為什么 Python 中缺少增量和減量運算符.
  • Python 使用 2個字節(jié)存儲函數中的本地變量. 理論上, 這意味著函數中只能定義65536個變量. 但是埃难,Python 內置了一個方便的解決方案,可用于存儲超過2^16個變量名. 下面的代碼演示了當定義了超過65536個局部變量時堆棧中發(fā)生的情況 (警告: 這段代碼會打印大約2^18行文本, 請做好準備!):

    import dis
    exec("""
    def f():
        """ + """
        """.join(["X"+str(x)+"=" + str(x) for x in range(65539)]))
    
    f()
    
    print(dis.dis(f))
    
  • 你的 Python 代碼 并不會多線程同時運行 (是的, 你沒聽錯!). 雖然你覺得會產生多個線程并讓它們同時執(zhí)行你的代碼, 但是, 由于 全局解釋鎖的存在, 你所做的只是讓你的線程依次在同一個核心上執(zhí)行. Python 多線程適用于IO密集型的任務, 但如果想要并行處理CPU密集型的任務, 你應該會想使用 multiprocessing 模塊.

  • 列表切片超出索引邊界而不引發(fā)任何錯誤

    >>> some_list = [1, 2, 3, 4, 5]
    >>> some_list[111:]
    []
    
  • int('?????????') 在 Python 3 中會返回 123456789. 在 Python 中, 十進制字符包括數字字符, 以及可用于形成十進制數字的所有字符, 例如: U+0660, ARABIC-INDIC DIGIT ZERO. 這有一個關于此的 有趣故事.

  • 'abc'.count('') == 4. 這有一個 count 方法的相近實現, 能更好的說明問題

    def count(s, sub):
        result = 0
        for i in range(len(s) + 1 - len(sub)):
            result += (s[i:i + len(sub)] == sub)
        return result
    

    這個行為是由于空子串('')與原始字符串中長度為0的切片相匹配導致的.


Some nice Links!/一些不錯的資源

Surprise your geeky pythonist friends?/想給你的極客朋友一個驚喜?

您可以使用這些快鏈向 Twitter 和 Linkedin 上的朋友推薦 wtfpython,

Twitter

Linkedin

中文版:

英文原版:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末涤久,一起剝皮案震驚了整個濱河市涡尘,隨后出現的幾起案子,更是在濱河造成了極大的恐慌拴竹,老刑警劉巖悟衩,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異栓拜,居然都是意外死亡座泳,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門幕与,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挑势,“玉大人,你說我怎么就攤上這事啦鸣〕北ィ” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵诫给,是天一觀的道長香拉。 經常有香客問我,道長中狂,這世上最難降的妖魔是什么凫碌? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮胃榕,結果婚禮上盛险,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好苦掘,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布换帜。 她就那樣靜靜地躺著,像睡著了一般鹤啡。 火紅的嫁衣襯著肌膚如雪惯驼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天揉忘,我揣著相機與錄音跳座,去河邊找鬼。 笑死泣矛,一個胖子當著我的面吹牛疲眷,可吹牛的內容都是我干的。 我是一名探鬼主播您朽,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼狂丝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了哗总?” 一聲冷哼從身側響起几颜,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讯屈,沒想到半個月后蛋哭,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡涮母,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年谆趾,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叛本。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡沪蓬,死狀恐怖,靈堂內的尸體忽然破棺而出来候,到底是詐尸還是另有隱情跷叉,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布营搅,位于F島的核電站云挟,受9級特大地震影響,放射性物質發(fā)生泄漏转质。R本人自食惡果不足惜植锉,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望峭拘。 院中可真熱鬧,春花似錦、人聲如沸鸡挠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拣展。三九已至彭沼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間备埃,已是汗流浹背姓惑。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留按脚,地道東北人于毙。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像辅搬,于是被迫代替她去往敵國和親唯沮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355