GitHub 上有一個(gè)名為《What the f*ck Python!》的項(xiàng)目,這個(gè)有趣的項(xiàng)目意在收集 Python 中那些難以理解和反人類直覺(jué)的例子以及鮮為人知的功能特性攒菠,并嘗試討論這些現(xiàn)象背后真正的原理迫皱!
原版地址:https://github.com/satwikkansal/wtfpython
最近,一位名為“暮晨”的貢獻(xiàn)者將其翻譯成了中文辖众。
中文版地址:https://github.com/leisurelicht/wtfpython-cn
我將所有代碼都親自試過(guò)了卓起,加入了一些自己的理解和例子,所以會(huì)和原文稍有不同凹炸。
1. 字符串駐留
①
>>> a = '!'
>>> b = '!'
>>> a is b
True
②
>>> a = 'some_string'
>>> id(a)
140420665652016
>>> id('some' + '_' + 'string') # 注意兩個(gè)的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
說(shuō)明:
這些行為是由于 CPython 在編譯優(yōu)化時(shí)戏阅,某些情況下會(huì)嘗試使用已經(jīng)存在的不可變對(duì)象而不是每次都創(chuàng)建一個(gè)新對(duì)象。這種行為被稱作字符串的駐留 string interning啤它。發(fā)生駐留之后, 許多變量可能指向內(nèi)存中的相同字符串對(duì)象從而節(jié)省內(nèi)存奕筐。
有一些方法可以用來(lái)猜測(cè)字符串是否會(huì)被駐留:
- 所有長(zhǎng)度為 0 和長(zhǎng)度為 1 的字符串都被駐留(①中字符串被駐留)
- 字符串在編譯時(shí)被實(shí)現(xiàn)(
'wtf'
將被駐留,但是''.join(['w', 't', 'f']
將不會(huì)被駐留) - 字符串中只包含字母变骡、數(shù)字或下劃線時(shí)將會(huì)駐留离赫,所以
'wtf!'
由于包含'!'
而未被駐留 - 當(dāng)在同一行將
a
和b
的值設(shè)置為'wtf!'
的時(shí)候,Python 解釋器會(huì)創(chuàng)建一個(gè)新對(duì)象塌碌,然后兩個(gè)變量同時(shí)指向這個(gè)對(duì)象渊胸。如果你在不同的行上進(jìn)行賦值操作,它就不會(huì)“知道”已經(jīng)有一個(gè)'wtf!'
對(duì)象(因?yàn)?'wtf!'
不是按照上面提到的方式被隱式駐留的)台妆。 - 常量折疊(constant folding)是 Python 中的一種窺孔優(yōu)化(peephole optimization)技術(shù)翎猛。這意味著在編譯時(shí)表達(dá)式
'a' * 20
會(huì)被替換為'aaaaaaaaaaaaaaaaaaaa'
以減少運(yùn)行時(shí)的時(shí)鐘周期瓢捉。只有長(zhǎng)度小于 20 的字符串才會(huì)發(fā)生常量折疊。(為啥办成?想象一下由于表達(dá)式'a' * 10 ** 10
而生成的 .pyc 文件的大信萏)。
如果你在 .py 文件中嘗試這個(gè)例子迂卢,則不會(huì)看到相同的行為某弦,因?yàn)槲募且淮涡跃幾g的。
2. 字典的鍵
>>> 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"
說(shuō)明:
Python 字典檢查鍵值是否相等是通過(guò)比較哈希值是否相等來(lái)確定的而克。如果兩個(gè)對(duì)象在比較的時(shí)候是相等的靶壮,那它們的散列值必須相等,否則散列表就不能正常運(yùn)行了员萍。例如腾降,如果 1 == 1.0
為真,那么 hash(1) == hash(1.0)
必須也為真碎绎,但其實(shí)兩個(gè)數(shù)字(整數(shù)和浮點(diǎn)數(shù))的內(nèi)部結(jié)構(gòu)是完全不一樣的螃壤。
3. finally 子句中的 return
def some_func():
try:
return 'from_try'
finally:
return 'from_finally'
Output:
>>> some_func()
'from_finally'
說(shuō)明:
函數(shù)的返回值由最后執(zhí)行的 return
語(yǔ)句決定。由于 finally
子句一定會(huì)執(zhí)行筋帖,所以 finally
子句中的 return
將始終是最后執(zhí)行的語(yǔ)句奸晴。
4. 同一個(gè)對(duì)象
class WTF:
pass
Output:
>>> WTF() == WTF() # 兩個(gè)不同的對(duì)象應(yīng)該不相等
False
>>> WTF() is WTF() # 也不相同
False
>>> hash(WTF()) == hash(WTF()) # 哈希值也應(yīng)該不同
True
>>> id(WTF()) == id(WTF())
True
說(shuō)明:
當(dāng)調(diào)用 id()
函數(shù)時(shí),Python 創(chuàng)建了一個(gè) WTF
類的對(duì)象并傳給 id()
函數(shù)日麸,然后 id()
函數(shù)獲取其 id 值(也就是內(nèi)存地址)寄啼,然后丟棄該對(duì)象,該對(duì)象就被銷毀了代箭。
當(dāng)我們連續(xù)兩次進(jìn)行這個(gè)操作時(shí)墩划,Python會(huì)將相同的內(nèi)存地址分配給第二個(gè)對(duì)象,因?yàn)樵?CPython 中 id()
函數(shù)使用對(duì)象的內(nèi)存地址作為對(duì)象的 id 值嗡综,所以兩個(gè)對(duì)象的 id 值是相同的乙帮。
綜上,對(duì)象的 id 值僅僅在對(duì)象的生命周期內(nèi)唯一蛤高,在對(duì)象被銷毀之后或被創(chuàng)建之前蚣旱,其他對(duì)象可以具有相同的 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
正如你所看到的戴陡,對(duì)象銷毀的順序是造成所有不同之處的原因塞绿。
5. for 循環(huán)分配目標(biāo)賦值
>>> some_string = "wtf"
>>> some_dict = {}
>>> for i, some_dict[i] in enumerate(some_string): pass
>>> some_dict
{0: 'w', 1: 't', 2: 'f'}
說(shuō)明:
這一條仔細(xì)看一下很好理解,for
循環(huán)每次迭代都會(huì)給分配目標(biāo)賦值恤批,some_dict[i] = value
就相當(dāng)于給字典添加鍵值對(duì)了异吻。
有趣的是下面這個(gè)例子,你可曾覺(jué)得這個(gè)循環(huán)只會(huì)運(yùn)行一次?
for i in range(4):
print(i)
i = 10
6. 執(zhí)行時(shí)機(jī)差異
①
>>> 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]
說(shuō)明:
在生成器表達(dá)式中 in
子句在聲明時(shí)執(zhí)行诀浪,而條件子句則是在運(yùn)行時(shí)執(zhí)行棋返。
①中,在運(yùn)行前 array
已經(jīng)被重新賦值為 [2, 8, 22]
雷猪,因此對(duì)于之前的 1, 8, 15睛竣,只有 count(8)
的結(jié)果是大于 0 ,所以生成器只會(huì)生成 8求摇。
②中射沟,g1
和 g2
的輸出差異則是由于變量 array_1
和 array_2
被重新賦值的方式導(dǎo)致的。
- 在第一種情況下与境,
array_1
被綁定到新對(duì)象[1, 2, 3, 4, 5]
验夯,因?yàn)?in
子句是在聲明時(shí)被執(zhí)行的,所以它仍然引用舊對(duì)象[1, 2, 3, 4]
(并沒(méi)有被銷毀)摔刁。 - 在第二種情況下挥转,對(duì)
array_2
的切片賦值將相同的舊對(duì)象[1, 2, 3, 4]
原地更新為[1, 2, 3, 4, 5]
。因此 g2 和array_2
仍然引用同一個(gè)對(duì)象[1, 2, 3, 4, 5]
共屈。
7. 整數(shù)的預(yù)分配
>>> 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
運(yùn)算符檢查兩個(gè)運(yùn)算對(duì)象是否引用自同一對(duì)象 -
==
運(yùn)算符比較兩個(gè)運(yùn)算對(duì)象的值是否相等
因此 is
代表引用相同绑谣,==
代表值相等。下面的例子可以很好的說(shuō)明這點(diǎn):
>>> [] == []
True
>>> [] is [] # 這兩個(gè)空列表位于不同的內(nèi)存地址
False
256 是一個(gè)已經(jīng)存在的對(duì)象趁俊,而 257 不是
當(dāng)啟動(dòng) Python 的時(shí)候域仇,-5 到 256 的數(shù)值就已經(jīng)被分配好了刑然。這些數(shù)字因?yàn)榻?jīng)常使用所以適合被提前準(zhǔn)備好寺擂。
當(dāng)前的實(shí)現(xiàn)為 -5 到 256 之間的所有整數(shù)保留一個(gè)整數(shù)對(duì)象數(shù)組,當(dāng)你創(chuàng)建了一個(gè)該范圍內(nèi)的整數(shù)時(shí)泼掠,你只需要返回現(xiàn)有對(duì)象的引用怔软。所以改變 1 的值是有可能的。
但是择镇,當(dāng) a
和 b
在同一行中使用相同的值初始化時(shí)挡逼,會(huì)指向同一個(gè)對(duì)象。
>>> 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
這是一種特別為交互式環(huán)境做的編譯器優(yōu)化腻豌,當(dāng)你在實(shí)時(shí)解釋器中輸入兩行的時(shí)候家坎,他們會(huì)單獨(dú)編譯,因此也會(huì)單獨(dú)進(jìn)行優(yōu)化吝梅, 如果你在 .py 文件中嘗試這個(gè)例子虱疏,則不會(huì)看到相同的行為,因?yàn)槲募且淮涡跃幾g的苏携。
8. 容易疏忽的引用類型賦值
>>> row = [''] * 3
>>> board = [row] * 3
>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]
說(shuō)明:
我們來(lái)輸出 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
是一個(gè) list做瞪,其中三個(gè)元素都指向地址 5143216,當(dāng)對(duì) board[0][0]
進(jìn)行賦值以后,row
的第一個(gè)元素指向 7536232装蓬。而 board
中的三個(gè)元素都指向 row
著拭,row
的地址并沒(méi)有改變。
我們可以通過(guò)不使用變量 row
生成 board
來(lái)避免這種情況牍帚。
>>> board = [[''] * 3 for _ in range(3)]
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['', '', ''], ['', '', '']]
這里用了推導(dǎo)式儡遮,每次迭代都會(huì)生成一個(gè)新的 _
,所以 board
中三個(gè)元素指向的是不同的變量暗赶。
9. 閉包函數(shù)
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]
說(shuō)明:
當(dāng)在循環(huán)內(nèi)部定義一個(gè)函數(shù)時(shí)峦萎,如果該函數(shù)在其主體中使用了循環(huán)變量,則閉包函數(shù)將與循環(huán)變量綁定忆首,而不是它的值爱榔。因此,所有的函數(shù)都是使用最后分配給變量的值來(lái)進(jìn)行計(jì)算的糙及。
可以通過(guò)將循環(huán)變量作為命名變量傳遞給函數(shù)來(lái)獲得預(yù)期的結(jié)果详幽。為什么這樣可行?因?yàn)檫@會(huì)在函數(shù)內(nèi)再次定義一個(gè)局部變量浸锨。
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. 字符串末尾的反斜杠
>>> print("\\ C:\\")
\ C:\
>>> print(r"\ C:")
\ C:
>>> print(r"\ C:\")
File "<stdin>", line 1
print(r"\ C:\")
^
SyntaxError: EOL while scanning string literal
說(shuō)明:
在以 r
開(kāi)頭的原始字符串中唇聘,反斜杠并沒(méi)有特殊含義。解釋器所做的只是簡(jiǎn)單的改變了反斜杠的行為柱搜,因此會(huì)直接傳遞反斜杠及后一個(gè)的字符迟郎。這就是反斜杠在原始字符串末尾不起作用的原因。
11. == 和 not 運(yùn)算符的優(yōu)先級(jí)
>>> not x == y
True
>>> x == not y
File "<input>", line 1
x == not y
^
SyntaxError: invalid syntax
說(shuō)明:
一句話聪蘸,==
運(yùn)算符的優(yōu)先級(jí)要高于 not
運(yùn)算符宪肖。
12. 三引號(hào)
>>> print('wtfpython''')
wtfpython
>>> print("wtfpython""")
wtfpython
>>> # 下面的語(yǔ)句會(huì)拋出 `SyntaxError` 異常
>>> # print('''wtfpython')
>>> # print("""wtfpython")
說(shuō)明:
'''
和 """
在 Python 中也是字符串定界符,Python 解釋器在先遇到三個(gè)引號(hào)的的時(shí)候會(huì)嘗試再尋找三個(gè)終止引號(hào)作為定界符健爬,如果不存在則會(huì)導(dǎo)致 SyntaxError
異常控乾。
而 Python 提供隱式的字符串鏈接:
>>> print("wtf" "python")
wtfpython
>>> print("wtf""") # 相當(dāng)于 "wtf" ""
wtf
13. 消失的午夜0點(diǎn)
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
并沒(méi)有被輸出。
說(shuō)明:
在 Python 3.5 之前娜遵,如果 datetime.time
對(duì)象存儲(chǔ)的 UTC 的午夜 0 點(diǎn), 那么它的布爾值會(huì)被認(rèn)為是 False蜕衡。
這個(gè)我特意下了個(gè) python 3.4 驗(yàn)證了下,真是這樣设拟。
14. bool 值
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
說(shuō)明:
布爾值是 int
的子類
>>> isinstance(True, int)
True
>>> isinstance(False, int)
True
在引入實(shí)際 bool
類型之前慨仿,0 和 1 是真值的官方表示。為了向下兼容纳胧,新的 bool
類型需要像 0 和 1 一樣工作镰吆。
15. 類屬性和實(shí)例屬性
①
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
說(shuō)明:
- 類變量和實(shí)例變量在內(nèi)部是通過(guò)類對(duì)象的字典來(lái)處理(
__dict__
屬性),如果在當(dāng)前類的字典中找不到的話就去它的父類中尋找躲雅。 -
+=
運(yùn)算符會(huì)在原地修改可變對(duì)象鼎姊,而不是創(chuàng)建新對(duì)象。因此,修改一個(gè)實(shí)例的屬性會(huì)影響其他實(shí)例和類屬性相寇。
16. yield 的 bug
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']
說(shuō)明:
這是 CPython 在理解和生成器表達(dá)式中處理 yield
的一個(gè)錯(cuò)誤慰于,在 Python 3.8 中修復(fù),在 Python 3.7 中有棄用警告唤衫。 請(qǐng)參閱 Python 錯(cuò)誤報(bào)告和 Python 3.7 和 Python 3.8 的新增條目婆赠。
來(lái)源和解釋可以在這里找到: https://stackoverflow.com/questions/32139885/yield-in-list-comprehensions-and-generator-expressions
相關(guān)錯(cuò)誤報(bào)告: http://bugs.python.org/issue10544
17. 元組的相對(duì)不可變性
>>> 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) # 這里不出現(xiàn)錯(cuò)誤
>>> 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])
說(shuō)明:
元組中不可變的元素的標(biāo)識(shí)(即元素的地址),如果元素是引用類型佳励,元組的值會(huì)隨著引用的可變對(duì)象的變化而變化休里。所以 another_tuple[2].append(1000)
是可以的。
+=
操作符在原地修改了列表赃承。元素賦值操作并不工作妙黍,但是當(dāng)異常拋出時(shí),元素已經(jīng)在原地被修改了瞧剖。+=
并不是原子操作拭嫁,而是 extend
和 =
兩個(gè)動(dòng)作,這里 =
操作雖然會(huì)拋出異常抓于,但 extend
操作已經(jīng)修改成功了做粤。
18. 消失的外部變量
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
說(shuō)明:
當(dāng)使用 as
為目標(biāo)分配異常的時(shí)候,將在 except
子句的末尾清除該異常捉撮。
這就好像:
except E as N:
foo
會(huì)被翻譯成:
except E as N:
try:
foo
finally:
del N
這意味著必須將異常分配給其他名稱才能在 except
子句之后引用它怕品。而異常之所以會(huì)被清除,是因?yàn)楦郊恿嘶厮菪畔ⅲ?em>trackback)巾遭,它們與棧幀(stack frame)形成一個(gè)引用循環(huán)肉康,使得該棧幀中的所有本地變量在下一次垃圾回收發(fā)生之前都處于活動(dòng)狀態(tài)(不會(huì)被回收)。
子句在 Python 中并沒(méi)有獨(dú)立的作用域恢总。示例中的所有內(nèi)容都處于同一作用域內(nèi)迎罗,所以變量 e
會(huì)由于執(zhí)行了 except
子句而被刪除。而對(duì)于有獨(dú)立的內(nèi)部作用域的函數(shù)來(lái)說(shuō)情況就不一樣了片仿。下面的例子說(shuō)明了這一點(diǎn):
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]
19. bool 類型
True = False
if True == False:
print("I've lost faith in truth!")
Output:
I've lost faith in truth!
說(shuō)明:
最初,Python 并沒(méi)有 bool
型(人們用 0 表示假值, 用非零值比如 1 作為真值)尤辱。后來(lái)他們添加了 True
, False
, 和 bool
型砂豌,但是,為了向后兼容光督,他們沒(méi)法把 True
和 False
設(shè)置為常量阳距,只是設(shè)置成了內(nèi)置變量。
Python 3 由于不再需要向后兼容结借,終于可以修復(fù)這個(gè)問(wèn)題了筐摘,所以這個(gè)例子無(wú)法在 Python 3.x 中執(zhí)行。
20. append 方法陷阱
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
說(shuō)明:
大多數(shù)修改序列/映射對(duì)象的方法,比如 list.append
咖熟,dict.update
圃酵,list.sort
等等,都是原地修改對(duì)象并返回 None
馍管,這樣可以避免創(chuàng)建對(duì)象的副本來(lái)提高性能郭赐。