1. 程式人生 > >Python:What the f*ck Python(上)

Python:What the f*ck Python(上)

GitHub 上有一個名為《What the f*ck Python!》的專案,這個有趣的專案意在收集 Python 中那些難以理解和反人類直覺的例子以及鮮為人知的功能特性,並嘗試討論這些現象背後真正的原理!
原版地址:https://github.com/satwikkansal/wtfpython

最近,一位名為“暮晨”的貢獻者將其翻譯成了中文。
中文版地址:https://github.com/leisurelicht/wtfpython-cn

我將所有程式碼都親自試過了,加入了一些自己的理解和例子,所以會和原文稍有不同

1. Strings can be tricky sometimes

>>> a = '!'
>>> b = '!'
>>> a is b
True

>>> a = 'some_string'
>>> id(a)
140420665652016
>>> id('some' + '_' + 'string') # 注意兩個的id值是相同的.
140420665652016

>>> a = 'wtf'
>>> b = 'wtf'
>>> a is b
True

>>> a = 'wtf!'
>>> b = 'wtf!'
>>> a is b
False

>>> a, b = 'wtf!', 'wtf!'
>>> a is b
True

>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False

說明:
這些行為是由於 Cpython 在編譯優化時,某些情況下會嘗試使用已經存在的不可變物件而不是每次都建立一個新物件。(這種行為被稱作字串的駐留[string interning])。發生駐留之後, 許多變數可能指向記憶體中的相同字串物件從而節省記憶體。

有一些方法可以用來猜測字串是否會被駐留:

  • 所有長度為 0 和長度為 1 的字串都被駐留(①中字串被駐留)
  • 字串在編譯時被實現('wtf' 將被駐留,但是 ''.join(['w', 't', 'f'] 將不會被駐留)
  • 字串中只包含字母、數字或下劃線時將會駐留,所以 'wtf!' 由於包含 '!' 而未被駐留
  • 當在同一行將 a 和 b 的值設定為 "wtf!" 的時候,Python 直譯器會建立一個新物件,然後兩個變數同時指向這個物件。如果你在不同的行上進行賦值操作,它就不會“知道”已經有一個 wtf! 物件(因為 "wtf!" 不是按照上面提到的方式被隱式駐留的)。
  • 常量摺疊(constant folding)是 Python 中的一種窺孔優化(peephole optimization)技術。這意味著在編譯時表示式 'a' * 20 會被替換為 'aaaaaaaaaaaaaaaaaaaa' 以減少執行時的時鐘週期。只有長度小於 20 的字串才會發生常量摺疊。(為啥?想象一下由於表示式 'a' * 10 ** 10 而生成的 .pyc 檔案的大小)。

如果你在 .py 檔案中嘗試這個例子,則不會看到相同的行為,因為檔案是一次性編譯的。

2. Time for some hash brownies!

>>> some_dict = {}
>>> some_dict[5.5] = "Ruby"
>>> some_dict[5.0] = "JavaScript"
>>> some_dict[5] = "Python"

>>> some_dict[5.5]
"Ruby"
>>> some_dict[5.0]
"Python"
>>> some_dict[5]
"Python"

說明:
Python 字典檢查鍵值是否相等是通過比較雜湊值是否相等來確定的。如果兩個物件在比較的時候是相等的,那它們的雜湊值必須相等,否則散列表就不能正常運行了。例如,如果 1 == 1.0 為真,那麼 hash(1) == hash(1.0) 必須也為真,但其實兩個數字(整數和浮點數)的內部結構是完全不一樣的。

3. Return return everywhere!

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

Output:

>>> some_func()
'from_finally'

說明:
函式的返回值由最後執行的 return 語句決定。由於 finally 子句一定會執行,所以 finally 子句中的 return 將始終是最後執行的語句。

4. 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 建立了一個 WTF 類的物件並傳給 id 函式,然後 id 函式獲取其 id 值(也就是記憶體地址),然後丟棄該物件,該物件就被銷燬了。

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

綜上,物件的 id 值僅僅在物件的生命週期內唯一,在物件被銷燬之後或被建立之前,其他物件可以具有相同的id值。

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

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

5. For what?

>>> some_string = "wtf"
>>> some_dict = {}
>>> for i, some_dict[i] in enumerate(some_string): pass
>>> some_dict
{0: 'w', 1: 't', 2: 'f'}

說明:
這一條仔細看一下很好理解,for 迴圈每次迭代都會給分配目標賦值,some_dict[i] = value 就相當於給字典新增鍵值對了。
有趣的是下面這個例子,你可曾覺得這個迴圈只會執行一次?

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

6. Evaluation time discrepancy

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

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

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

7. is is not what it 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 和 == 的區別

  • is 運算子檢查兩個運算物件是否引用自同一物件
  • == 運算子比較兩個運算物件的值是否相等

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

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

256 是一個已經存在的物件,而 257 不是
當啟動 Python 的時候,-5 到 256 的數值就已經被分配好了。這些數字因為經常使用所以適合被提前準備好。

當前的實現為 -5 到 256 之間的所有整數保留一個整數物件陣列,當你建立了一個該範圍內的整數時,你只需要返回現有物件的引用。所以改變 1 的值是有可能的。

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

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

>>> a, b = 257, 257
>>> id(a)
140640774013296
>>> id(b)
140640774013296

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

8. A tic-tac-toe where X wins in the first attempt!

>>> row = [''] * 3
>>> board = [row] * 3
>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]

說明:
我們來輸出 id 看下:

>>> id(row[0])
7536232
>>> id(row[1])
5143216
>>> id(row[2])
5143216
>>> id(board[0])
7416840
>>> id(board[1])
7416840
>>> id(board[2])
7416840

row 是一個 list,其中三個元素都指向地址 5143216,當對 board[0][0] 進行賦值以後,row 的第一個元素指向 7536232。而 board 中的三個元素都指向 row,row 的地址並沒有改變。

我們可以通過不使用變數 row 生成 board 來避免這種情況。

>>> board = [[''] * 3 for _ in range(3)]
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['', '', ''], ['', '', '']]

這裡用了推導式,每次迭代都會生成一個新的 _ ,所以 board 中三個元素指向的是不同的變數。

9. 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]

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

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

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]

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

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

說明:
is not 是個單獨的二元運算子,與分別使用 is 和 not 不同。

11. The surprising comma

略過,我想沒人會在函式的最後一個引數後面再加一個逗號吧!
況且,尾隨逗號的問題已經在 Python 3.6 中被修復了。

12. Backslashes at the end of string

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

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

說明:
在以 r 開頭的原始字串中,反斜槓並沒有特殊含義。直譯器所做的只是簡單的改變了反斜槓的行為,因此會直接傳遞反斜槓及後一個的字元。這就是反斜槓在原始字串末尾不起作用的原因。

13. not knot!

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

說明:
一句話,== 運算子的優先順序要高於 not 運算子。

14. Half triple-quoted strings

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

說明:
''' 和 """ 在 Python中也是字串定界符,Python 直譯器在先遇到三個引號的的時候會嘗試再尋找三個終止引號作為定界符,如果不存在則會導致 SyntaxError 異常。

而 Python 提供隱式的字串連結:

>>> print("wtf" "python")
wtfpython
>>> print("wtf""")  # 相當於 "wtf" ""
wtf

15. 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 12:00:00

midnight_time 並沒有被輸出。
說明:
在Python 3.5之前,如果 datetime.time 物件儲存的UTC的午夜0點, 那麼它的布林值會被認為是 False。
這個我特意下了個 python 3.4 驗證了下,真是這樣。

16. What's wrong with booleans

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

說明:
布林值是 int 的子類

>>> isinstance(True, int)
True
>>> isinstance(False, int)
True

在引入實際 bool 型別之前,0 和 1 是真值的官方表示。為了向下相容,新的 bool 型別需要像 0 和 1 一樣工作。

17. Class attributes and instance attributes

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)

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__ 屬性),如果在當前類的字典中找不到的話就去它的父類中尋找。
  • += 運算子會在原地修改可變物件,而不是建立新物件。因此,修改一個例項的屬性會影響其他例項和類屬性。

    18. yielding 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']

說明:
這是CPython在理解和生成器表示式中處理yield的一個錯誤,在Python 3.8中修復,在Python 3.7中有棄用警告。 請參閱Python錯誤報告和Python 3.7和Python 3.8的新增條目。

來源和解釋可以在這裡找到: https://stackoverflow.com/questions/32139885/yield-in-list-comprehensions-and-generator-expressions
相關錯誤報告: http://bugs.python.org/issue10544

19. Mutating the immutable!

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

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

說明:
元組中不可變的元素的標識(即元素的地址),如果元素是引用型別,元組的值會隨著引用的可變物件的變化而變化。所以 another_tuple[2].append(1000) 是可以的。
+= 操作符在原地修改了列表。元素賦值操作並不工作,但是當異常丟擲時,元素已經在原地被修改了。+= 並不是原子操作,而是 extend 和 = 兩個動作,這裡 = 操作雖然會丟擲異常,但 extend 操作已經修改成功了。

20. The disappearing variable from outer scope

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

Output: python2

>>> print(e)
# prints nothing

Output: python3

>>> 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)形成一個引用迴圈,使得該棧幀中的所有本地變數在下一次垃圾回收發生之前都處於活動狀態(不會被回收)。

子句在 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]

21. 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 型,但是,為了向後相容,他們沒法把 True 和 False 設定為常量,只是設定成了內建變數。
Python 3 由於不再需要向後相容,終於可以修復這個問題了,所以這個例子無法在 Python 3.x 中執行。

22. 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,這樣可以避免建立物件的副本來提高效能。

23. Subclass relationships

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

子類關係應該是可傳遞的,對吧?即,如果 A 是 B 的子類,B 是 C 的子類,那麼 A 應該 是 C 的子類。
說明:

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

    24. The mysterious key type conversion

class SomeClass(str):
    pass

some_dict = {'s': 42}

Output:

>>> type(list(some_dict.keys())[0])
<class 'str'>
>>> s = SomeClass('s')
>>> some_dict[s] = 40
>>> some_dict # 預期: 兩個不同的鍵值對
{'s': 40}
>>> type(list(some_dict.keys())[0])
<class '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])
<class 'str'> <class '__main__.SomeClass'>

25. Let's see if you can guess this?

>>> a, b = a[b] = {}, 5
>>> a
{5: ({...}, 5)}

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

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

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

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

這話看著非常的晦澀,我們來看一個簡單的例子:

a, b = b, c = 1, 2
print(a, b, c)

Output:

1 1 2

在這個簡單的例子中,目標列表是 a, b 和 b, c,表示式是 1, 2。將表示式從左到右賦給目標列表,上述例子就可以拆分成:

a, b = 1, 2
b, c = 1, 2

所以結果就是 1 1 2。

那麼,原例子就不難理解了,拆解開來就是:

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

這裡不能寫作 a[b] = {}, 5,因為這樣第一句中的 {} 和第二句中的 {} 其實就是不同的物件了,而實際他們是同一個物件。這就形成了迴圈引用,輸出中的 {...} 指與 a 引用了相同的物件。
我們來驗證一下:

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

可見確實是同一個物件。

以下是一個簡單的迴圈引用的例子:

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