Python:不爲人知的功能特性(上)

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

最近,一位名爲「暮晨」的貢獻者將其翻譯成了中文。 中文版地址:github.com/leisurelich…git

本來每一個的標題都是原版中的英文,有些取名比較奇怪,不直觀,我換成了能夠描述主題的中文形式,有些是本身想的,不足之處請指正。另一些 Python 中的彩蛋被我去掉了。github

我將全部代碼都親自試過了,加入了一些本身的理解和例子,因此會和原文稍有不一樣。express

下篇已發佈:Python:不爲人知的功能特性(下)數組

1. 字符串駐留

bash

>>> a = '!'
>>> b = '!'
>>> a is b
True
複製代碼

閉包

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

app

>>> 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。發生駐留以後, 許多變量可能指向內存中的相同字符串對象從而節省內存。post

有一些方法能夠用來猜想字符串是否會被駐留:

  • 全部長度爲 0 和長度爲 1 的字符串都被駐留(①中字符串被駐留)
  • 字符串在編譯時被實現('wtf' 將被駐留,可是 ''.join(['w', 't', 'f'] 將不會被駐留)
  • 字符串中只包含字母、數字或下劃線時將會駐留,因此 'wtf!' 因爲包含 '!' 而未被駐留
  • 當在同一行將 ab 的值設置爲 'wtf!' 的時候,Python 解釋器會建立一個新對象,而後兩個變量同時指向這個對象。若是你在不一樣的行上進行賦值操做,它就不會「知道」已經有一個 'wtf!' 對象(由於 'wtf!' 不是按照上面提到的方式被隱式駐留的)。
  • 常量摺疊(constant folding)是 Python 中的一種窺孔優化(peephole optimization)技術。這意味着在編譯時表達式 'a' * 20 會被替換爲 'aaaaaaaaaaaaaaaaaaaa' 以減小運行時的時鐘週期。只有長度小於 20 的字符串纔會發生常量摺疊。(爲啥?想象一下因爲表達式 'a' * 10 ** 10 而生成的 .pyc 文件的大小)。

若是你在 .py 文件中嘗試這個例子,則不會看到相同的行爲,由於文件是一次性編譯的。

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"
複製代碼

說明: Python 字典檢查鍵值是否相等是經過比較哈希值是否相等來肯定的。若是兩個對象在比較的時候是相等的,那它們的散列值必須相等,不然散列表就不能正常運行了。例如,若是 1 == 1.0 爲真,那麼 hash(1) == hash(1.0) 必須也爲真,但其實兩個數字(整數和浮點數)的內部結構是徹底不同的。

3. finally 子句中的 return

def some_func():
    try:
        return 'from_try'
    finally:
        return 'from_finally'
複製代碼

Output:

>>> some_func()
'from_finally'
複製代碼

說明: 函數的返回值由最後執行的 return 語句決定。因爲 finally 子句必定會執行,因此 finally 子句中的 return 將始終是最後執行的語句。

4. 同一個對象

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會將相同的內存地址分配給第二個對象,由於在 CPythonid() 函數使用對象的內存地址做爲對象的 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 循環分配目標賦值

>>> 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. 執行時機差別

>>> 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。 ②中,g1g2 的輸出差別則是因爲變量 array_1array_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. 整數的預分配

>>> 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 的值是有可能的。

可是,當 ab 在同一行中使用相同的值初始化時,會指向同一個對象。

>>> 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. 容易疏忽的引用類型賦值

>>> 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 中的三個元素都指向 rowrow 的地址並無改變。

咱們能夠經過不使用變量 row 生成 board 來避免這種狀況。

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

這裏用了推導式,每次迭代都會生成一個新的 _ ,因此 board 中三個元素指向的是不一樣的變量。

9. 閉包函數

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. 字符串末尾的反斜槓

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

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

說明: 在以 r 開頭的原始字符串中,反斜槓並無特殊含義。解釋器所作的只是簡單的改變了反斜槓的行爲,所以會直接傳遞反斜槓及後一個的字符。這就是反斜槓在原始字符串末尾不起做用的緣由。

11. == 和 not 運算符的優先級

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

說明: 一句話,== 運算符的優先級要高於 not 運算符。

12. 三引號

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

說明: '''"""Python 中也是字符串定界符,Python 解釋器在先遇到三個引號的的時候會嘗試再尋找三個終止引號做爲定界符,若是不存在則會致使 SyntaxError 異常。

而 Python 提供隱式的字符串連接:

>>> print("wtf" "python")
wtfpython
>>> print("wtf""")  # 至關於 "wtf" ""
wtf
複製代碼

13. 消失的午夜0點

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 驗證了下,真是這樣。

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
複製代碼

說明: 布爾值是 int 的子類

>>> isinstance(True, int)
True
>>> isinstance(False, int)
True
複製代碼

在引入實際 bool 類型以前,0 和 1 是真值的官方表示。爲了向下兼容,新的 bool 類型須要像 0 和 1 同樣工做。

15. 類屬性和實例屬性

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__ 屬性),若是在當前類的字典中找不到的話就去它的父類中尋找。
  • += 運算符會在原地修改可變對象,而不是建立新對象。所以,修改一個實例的屬性會影響其餘實例和類屬性。

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']
複製代碼

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

來源和解釋能夠在這裏找到: stackoverflow.com/questions/3… 相關錯誤報告: bugs.python.org/issue10544

17. 元組的相對不可變性

>>> 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 操做已經修改爲功了。

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
複製代碼

說明: 當使用 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]
複製代碼

19. bool 類型

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 中執行。

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
複製代碼

說明: 大多數修改序列/映射對象的方法,好比 list.appenddict.updatelist.sort 等等,都是原地修改對象並返回 None,這樣能夠避免建立對象的副原本提升性能。


掃碼關注個人我的公衆號
相關文章
相關標籤/搜索