99% 人會做錯的python題


本文含 2693 5 圖表截屏

建議閱讀 15 分鐘

引言

題目:在不運行下面代碼(Python 3 不是 Python 2)的情況下選擇答案。

def f( x=[] ):
    x.append(1)
    return x
 
print( f(), f() )

很多人選第二個吧,[1] [1]。理由如下:一開始 x 是空列表,添加一個 1 不就是 [1] 嗎?然後函數 f() 運行兩遍。

還有人會選第三個吧,[1] [1, 1]。能選第三個的已經很強了,至少了解列表是可更改對象(mutable object),作爲函數的默認參數(default argument)每次會更新參數的默認值。很棒,但是忽略了 print() 函數的運行機制。

答案是第四個, [1, 1] [1, 1]。

如果你選了第四個,你可以關閉頁面,如果不是,請往下看,絕對能學到一些新的知識。你能需要的知識點是:

  1. 瞭解什麼是不可更改對象可更改對象

  2. 瞭解函數的默認參數

  3. 瞭解在 Python 3 裏面 print() 是函數而不是語句(在 Python 2 裏是語句)

不可更改對象 VS 可更改對象

判斷一個數據類型 X 是不是可更改的呢?兩種方法:

  • 方法一:用 id(X) 函數,對 X 進行某種操作,比較操作前後的 id,如果不一樣,則 X 不可更改,如果一樣,則 X 可更改

  • 方法二:用 hash(X) 函數,只要不報錯,證明 X 可被哈希,即不可更改,反過來不可被哈希,即可更改

我們用方法一,id() 函數,來驗證整數和列表是否可更改。

先看整數 i:

i = 1print( id(i) )i = i + 2print( id(i) )

1607630928
1607630992

整數 i 在加 1 之後的 id 和之前不一樣,因此加完之後的這個 i (雖然名字沒變),但是不是加前的那個 i 了,因此整數是不可更改的。

下圖給上述過程做了可視化,在 Python 中,給 i 賦值 1 其實是創建一個 PyObject(有個字段存儲的值爲 1),然後將變量 i 指向這個 PyObject。當更新 i = i + 2 時,其實是新創建了個 PyObject(有個字段存儲的值爲 3),而將變量 i 指向新的 PyObject,舊的 PyObject 最後會被“回收”。

從新建 PyObject 這個特點可看出,整數不能更改。

再看列表 l:

l = [1, 10.31]print( id(l) )l.append('Python')print( id(l) )

2022027856840
2022027856840

列表 l 在附加 'Python' 之後的 id 和之前一樣,因此列表是可更改的。

下圖給上述過程做了可視化。我們發現,列表作爲容器型數據,它本身的 PyObject,在添加或者刪除元素的時候,沒有改變。換句話說,列表是可更改的。

函數默認參數

先回顧一下題目中的代碼:

def f( x=[] ):
    x.append(1)
    return x

在函數 f 中,x 是默認參數,默認值是空列表

在 Python 中

默認參數值只能被初始化一次

如果使用可更改對象作爲默認參數,那麼被更改後的值將一直保留。

那麼下面代碼的運行結果就好理解了吧(注意我先用兩個 print 函數打印 f() 值)。

print( f() )
print( f() )
[1]
[1, 1]

在運行第一行代碼時,沒有給參數值,則用其默認值 [],然後添加元素 1,結果是 [1],沒任何問題。

在運行第二行代碼時,也沒有給參數值,還是用其默認值,但這個時候默認參數的類型是可更改的列表,它在第一次運行函數 f() 的時候已經變成了 [1],而這個 [1] 就更新爲默認值了。再添加元素 1,結果爲 [1, 1]。

So far so good,那爲什麼兩個 f() 一起打印出來會得到 [1,1] [1,1] 呢?

print( f(), f() )
[1, 1] [1, 1]

這就需要了解一下 print() 函數的細節了。

print() 函數

在 Python 3 中,print() 是個函數 (function) 而不是語句 (statement)。因此 print() 函數的所有參數要

在調用函數前先被估值

因此

    print( f(), f() ) 

等價於

    x1 = f()

    x2 = f()

    print( x1, x2 )


解釋如下:

  • 第一次調用 f() 產生 PyObject 並賦值給 x1 時,x1 指向 PyObject 而且其值爲 [1]

  • 第二次調用 f() 賦值給 x2 時,PyObject 裏的值更新爲 [1, 1],而 x1 和 x2 指向它,因此兩個值都更新爲 [1, 1]

用一張圖可視化下上述過程:

用代碼驗證一下,注意 id(x1) 和 id(x2) 一樣,就是說 x1 和 x2 指向同一個 PyObject。

x1 = f()
x2 = f()
print( id(x1), id(x2) )
print( x1, x2 )
2457681941960 2457681941960
[1, 1] [1, 1]

由於我們在調用 print() 函數前就完成了對 x1 和 x2 的評估,所以它們的值都爲 [1, 1]。

如果分開調用 print(f()) 呢?那麼結果就是 [1] [1,1] 了。看代碼:

print( f() )
print( f() )
[1]
[1, 1]

爲什麼結果不是 [1, 1] [1, 1] 呢?看下面的等價代碼先:

x1 = f()
print( x1 )


x2 = f()
print( x2 )
[1]
[1, 1]

不難發現,我們在評估 x2 之前就把 x1 的值 [1] 打印出來了,在評估 x2 之後 x1 也更新成 [1,1] 但是沒打印出來,不過我們可以驗證一下是不是這樣。

x1 = f()
print( x1 )


x2 = f()
print( x2 )


print( x1 )
[1]
[1, 1]
[1, 1]

到現在你應該明晰所有難點了吧。再回到開始,其實我們就是希望這個函數就是在傳入參數的列表上添加一個元素 1,那麼怎麼操作呢?用 None

None

None 和整數、浮點數、布爾一樣,是一種數據類型,而且不可更改,它的類型是 NoneType。

type(None)
NoneType

正因爲它的不可更改性質,如果你在函數中需要傳入一個默認參數值,用 None !

因此上面代碼更改成:

def f( x=None ):
    if x is None:
        x = []
    x.append(1)
    return x

核心點是“如果 x 值是 None ,那麼重新給 x 賦值一個空列表 []”。

再運行結果正常。

print( f(), f() )
[1] [1]

總結

你看,一個小題目能引出這麼多 Python 的細節知識點(如變量是指針PyObject(不)可更改對象函數默認參數print 函數內部機制NoneType 變量),而且這些知識點很多人都沒有深挖過。我覺得這個題目作爲面試題挺合適的,不要求你能完全做對,但在分析的過程可以檢查你對基本知識點的理解有多深。

朋友們,這道題你做對了嗎?如果做錯了現在學到新知識點了嗎?五一快樂!

歡迎添加小編微信,一起交流學習:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章