GitHub 上有一個名為《What the f*ck Python!》的項目,這個有趣的項目意在收集 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