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ī)則的實現.
- 當在同一行將
a
和b
的值設置為"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-29469 和 bpo-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將5
和5.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
,break
或continue
后,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_dict
鍵i
(剛剛分配的) 的值設為該字符. 本例中循環(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
,8
和15
, 只有count(8)
的結果是大于0
的, 所以生成器只會生成8
. - 第二部分中
g1
和g2
的輸出差異則是由于變量array_1
和array_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]
. 因此g2
和array_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 的時候, 數值為 -5
到 256
的對象就已經被分配好了. 這些數字因為經常被使用, 所以會被提前準備好.
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
, 所以它在內存中又新建了另一個對象.
當 a
和 b
在同一行中使用相同的值初始化時,會指向同一個對象.
>>> 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
是個單獨的二元運算符, 與分別使用is
和not
不同.- 如果操作符兩側的變量指向同一個對象, 則
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']
?? 說明:
- 來源和解釋可以在這里找到
- 相關錯誤報告
- 這個bug在3.7以后的版本中不被推薦使用, 并在3.8中被修復. 因此在3.8中嘗試在推導式中使用 yield, 只會得到一個 SyntaxError. 詳細內容可以看3.7更新內容, 3.8更新內容.
> 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
型, 但是, 為了向后兼容, 他們沒法把True
和False
設置為常量, 只是設置成了內置變量. - 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
子類關系應該是可傳遞的, 對吧? (即, 如果 A
是 B
的子類, B
是 C
的子類, 那么 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, b
和a[b]
(注意表達式列表只能有一個, 在我們的例子中是{}, 5
).表達式列表計算結束后, 將其值自動解包后從左到右分配給目標列表(target list). 因此, 在我們的例子中, 首先將
{}, 5
元組并賦值給a, b
, 然后我們就可以得到a = {}
且b = 5
.a
被賦值的{}
是可變對象.第二個目標列表是
a[b]
(你可能覺得這里會報錯, 因為在之前的語句中a
和b
都還沒有被定義. 但是別忘了, 我們剛剛將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
, remove
和 pop
的不同:
del var_name
只是從本地或全局命名空間中刪除了var_name
(這就是為什么list_1
沒有受到影響).remove
會刪除第一個匹配到的指定值, 而不是特定的索引, 如果找不到值則拋出ValueError
異常.pop
則會刪除指定索引處的元素并返回它, 如果指定了無效的索引則拋出IndexError
異常.
為什么輸出是 [2, 4]
?
列表迭代是按索引進行的, 所以當我們從
list_2
或list_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" 函數, 所以a
和b
仍然指向已被修改的同一列表.
> 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 == c
或 0 <= 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 里沒有
-
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!/一些不錯的資源
- https://www.youtube.com/watch?v=sH4XF6pKKmk
- https://www.reddit.com/r/Python/comments/3cu6ej/what_are_some_wtf_things_about_python
- https://sopython.com/wiki/Common_Gotchas_In_Python
- https://stackoverflow.com/questions/530530/python-2-x-gotchas-and-landmines
- https://stackoverflow.com/questions/1011431/common-pitfalls-in-python
- https://www.python.org/doc/humor/
- https://www.codementor.io/satwikkansal/python-practices-for-efficient-code-performance-memory-and-usability-aze6oiq65
Surprise your geeky pythonist friends?/想給你的極客朋友一個驚喜?
您可以使用這些快鏈向 Twitter 和 Linkedin 上的朋友推薦 wtfpython,