Python面試必備的7大問題

yesye發表於2021-09-11

Python面試必備的7大問題

Python面試(一)交換變數值

平時時不時會面面實習生,大多數的同學在學校裡都已經掌握了Python。面試的時候要求同學們實現一個簡單的函式,交換兩個變數的值,大多數的同學給出的都是如下的答案:

31add80e870c0dafb42e7e779e69dde.png

實際上,Python中還有更簡潔的更具Python風格的實現,如下:

330fa475905704111e92a00cdb2d3be.png

相比前一種方法,後一種方法節省一箇中間變數,在效能上也優於前一種方法。

我們從Python的位元組碼來深入分析一下原因。

09a2cadf0a59b6a83b2f053f3375011.png

dis是個反彙編工具,將Python程式碼翻譯成位元組碼指令。這裡的輸出如下:

67f5d50ad196ca8e42663a3c4d5b8c7.png

透過位元組碼可以看到,swap1和swap2最大的區別在於,swap1中透過ROT_TWO交換棧頂的兩個元素實現x和y值的互換,swap2中引入了tmp變數,多了一次LOAD_FAST, STORE_FAST的操作。執行一個ROT_TWO指令比執行一個LOAD_FAST+STORE_FAST的指令快,這也是為什麼swap1比swap2效能更好的原因。

Python面試(二) is 和 == 的區別

面試實習生的時候,當問到 is 和 == 的區別時,很多同學都答不上來,搞不清兩者什麼時候返回一致,什麼時候返回不一致。本文我們來看一下這兩者的區別。

我們先來看幾個例子:

087aefa82d774c45ed3570638a53c4f.png

上面的輸出結果中為什麼有的 is 和 == 的結果相同,有的不相同呢?我們來看下官方文件中對於 is 和 == 的解釋。

官方文件中說 is 表示的是物件標示符(object identity),而 == 表示的是相等(equality)。is 的作用是用來檢查物件的標示符是否一致,也就是比較兩個物件在記憶體中的地址是否一樣,而 == 是用來檢查兩個物件是否相等。

我們在檢查 a is b 的時候,其實相當於檢查 id(a) == id(b)。而檢查 a == b 的時候,實際是呼叫了物件 a 的 __eq()__ 方法,a == b 相當於 a.__eq__(b)。

一般情況下,如果 a is b 返回True的話,即 a 和 b 指向同一塊記憶體地址的話,a == b 也返回True,即 a 和 b 的值也相等。

好了,看明白上面的解釋後,我們來看下前面的幾個例子:

188fc4efc274f38694d07df1e5ce76a.png

列印出 id(a) 和 id(b) 後就很清楚了。只要 a 和 b 的值相等,a == b 就會返回True,而只有 id(a) 和 id(b) 相等時,a is b 才返回 True。

這裡還有一個問題,為什麼 a 和 b 都是 "hello" 的時候,a is b 返回True,而 a 和 b都是 "hello world" 的時候,a is b 返回False呢?

這是因為前一種情況下Python的字串駐留機制起了作用。對於較小的字串,為了提高系統效能Python會保留其值的一個副本,當建立新的字串的時候直接指向該副本即可。所以 "hello" 在記憶體中只有一個副本,a 和 b 的 id 值相同,而 "hello world" 是長字串,不駐留記憶體,Python中各自建立了物件來表示 a 和 b,所以他們的值相同但 id 值不同。

同學指出:intern機制和字串長短無關,在互動模式下,每行字串字面量都會申請一個新字串,但是隻含大小寫字母、數字和下劃線的會被intern,也就是維護了一張dict來使得這些字串全域性唯一)

總結一下,is 是檢查兩個物件是否指向同一塊記憶體空間,而 == 是檢查他們的值是否相等。可以看出,is 是比 == 更嚴格的檢查,is 返回True表明這兩個物件指向同一塊記憶體,值也一定相同。

看到這裡,大家是不是搞懂了 is 和 == 的區別呢?

讓我們深入一步來思考一下下面這個問題:

Python裡和None比較時,為什麼是 is None 而不是 == None 呢?

Python面試(三)可變物件和不可變物件

上一個面試題:Python面試之 is 和 == 的區別的最後留了一個問題:

Python裡和None比較時,為什麼是 is None 而不是 == None 呢?

這是因為None在Python裡是個單例物件,一個變數如果是None,它一定和None指向同一個記憶體地址。而 == None背後呼叫的是__eq__,而__eq__可以被過載,下面是一個 is not None但 == None的例子:

6b3a9a1fd290c1537370978ff3a6a0b.png

好了,解答就到這裡,我們開始本篇的正題。

Python中有可變物件和不可變物件之分。可變物件建立後可改變但地址不會改變,即變數指向的還是原來的變數;不可變物件建立之後便不能改變,如果改變則會指向一個新的物件。

Python中dict、list是可變物件,str、int、tuple、float是不可變物件。

來看一個字串的例子:

c9f4ca1ae5385389dbfecb3ce3837a6.png

上面的例子裡,修改a指向的物件的值會導致丟擲異常。

執行 a = a + " world"時,先計算等號右邊的表示式,生成一個新的物件賦值到變數a,因此a指向的物件發生了改變,id(a) 的值也與原先不同。

再來看一個列表的例子:

5c3eb6970980a72d1eac87e63fbd416.png

上面對a修改元素、新增元素,變數a還是指向原來的物件。

將a賦值給b後,變數b和a都指向同一個物件,因此修改b的元素值也會影響a。

變數c是對b的切片操作的返回值,切片操作相當於淺複製,會生成一個新的物件,因此c指向的物件不再是b所指向的物件,對c的操作不會改變b的值。

理解了上面不可變物件和可變物件的區別後,我們再來看一個有趣的問題:

d9f3ac37253bcf6e4babdd5fe448acc.png

明明group1和group2是不同的物件(id值不同),為什麼呼叫group2的add_member方法會影響group1的members?

其中的奧妙就在於__init__函式的第二個引數是預設引數,預設引數的預設值在函式建立的時候就生成了,每次呼叫都是用了這個物件的快取。我們檢查id(group1.mebers)和id(group2.members),可以發現他們是相同的。

print(id(group1.members)) # 輸出 140127132522040
print(id(group2.members)) # 輸出 140127132522040

所以,group1.members和group2.members指向了同一個物件,對group2.members的修改也會影響group1.members。

那麼問題來了,怎樣修改程式碼才能解決上面預設引數的問題呢?

Python面試(四)連線字串用join還是+

上一個面試題:Python面試之可變物件和不可變物件的最後留了一個問題:

c378b1e9922d5f1880acecd158cc579.png

上述程式碼中預設引數值物件會被快取,造成Group型別的物件共享同一個members列表,怎樣才能解決這個問題呢?

其實很簡單,只要傳入None作為預設引數,在建立物件的時候動態生成列表,如下:

bb5deb5751a70fd1644b5588dea4742.png

這樣對於不同的group物件,它們的members也是不同的物件,所以不會再出現更新一個group物件的members也會更新另外一個group物件的members了。

本篇要講的是,連線字串的時候可以用join也可以用+,但這兩者有沒有區別呢?

我們先來看一下用join和+連線字串的例子:

f54e8def670712f202706bdb7d14e3f.png

兩者的結果是一樣,那麼考慮這樣一個問題,這兩者在效能上有區別嗎?

我們來做個實驗,比較下join和+的效能:

5e82fa6960161bc15219128f42eead1.png

上面的程式有如下的輸出:

join: 0.116944, plus: 0.394379

可以看到,join的效能明顯好於+。這是為什麼呢?

原因是這樣的,上一篇Python面試之可變物件和不可變物件中講過字串是不可變物件,當用運算子+連線字串的時候,每執行一次+都會申請一塊新的記憶體,然後複製上一個+操作的結果和本次操作的右運算子到這塊記憶體空間,因此用+連線字串的時候會涉及好幾次記憶體申請和複製。而join在連線字串的時候,會先計算需要多大的記憶體存放結果,然後一次性申請所需記憶體並將字串複製過去,這是為什麼join的效能優於+的原因。所以在連線字串陣列的時候,我們應考慮優先使用join。

Python面試(五)理解__new__和__init__的區別

很多同學都以為Python中的__init__是構造方法,但其實不然,Python中真正的構造方法是__new__。__init__和__new__有什麼區別?本文就來探討一下。

我們先來看一下__init__的用法:

cd1bb55d6e37e0e5a134d13b0a92bf0.png

上面的程式碼會輸出如下的結果:

31b9a82979777a232dc93befde29791.png

那麼我們思考一個問題,Python中要實現Singleton怎麼實現,要實現工廠模式怎麼實現?

用__init__函式似乎沒法做到呢~

實際上,__init__函式並不是真正意義上的建構函式,__init__方法做的事情是在物件建立好之後初始化變數。真正建立例項的是__new__方法。

我們來看下面的例子:

6b40da6bd3e2e379a14e0d9b6b8dc7a.png

上面的程式碼輸出如下的結果:

747c082b2accd64c7ac39fdcb581aee.png

上面的程式碼中例項化了一個Person物件,可以看到__new__和__init__都被呼叫了。__new__方法用於建立物件並返回物件,當返回物件時會自動呼叫__init__方法進行初始化。__new__方法是靜態方法,而__init__是例項方法。

好了,理解__new__和__init__的區別後,我們再來看一下前面提出的問題,用Python怎麼實現Singleton,怎麼實現工廠模式?

先來看Singleton:

d4ae6909f9cc222e3c21afaa064258d.png

上面的程式碼輸出:

ba7cebebfa70ef9c082c22048b4807d.png

可以看到s1和s2都指向同一個物件,實現了單例模式。

再來看下工廠模式的實現:

c1cf345d30f86c434405e4e89e5e055.png

上面的程式碼輸出:

40e904a8ab3836a13d41b9d4f09b020.png

看完上面兩個例子,大家是不是對__new__和__init__的區別有了更深入的理解?

Python面試(六)with與上下文管理器With基本語法

Python老司機應該對下面的語法不陌生:

81bfe62105e35028263f4b710a01ee1.png

上面的程式碼往output檔案寫入了Hello world字串,with語句會在執行完程式碼塊後自動關閉檔案。這裡無論寫檔案的操作成功與否,是否有異常丟擲,with語句都會保證檔案被關閉。

如果不用with,我們可能要用下面的程式碼實現類似的功能:

3eb9cec878911d04a581bc87b037e77.png

可以看到使用了with的程式碼比上面的程式碼簡潔許多。

上面的with程式碼背後發生了些什麼?我們來看下它的執行流程:

首先執行open('output', 'w'),返回一個檔案物件;

呼叫這個檔案物件的__enter__方法,並將__enter__方法的返回值賦值給變數f;

執行with語句體,即with語句包裹起來的程式碼塊;

不管執行過程中是否發生了異常,執行檔案物件的__exit__方法,在__exit__方法中關閉檔案。

這裡的關鍵在於open返回的檔案物件實現了__enter__和__exit__方法。一個實現了__enter__和__exit__方法的物件就稱之為上下文管理器。

上下文管理器

上下文管理器定義執行 with 語句時要建立的執行時上下文,負責執行 with 語句塊上下文中的進入與退出操作。__enter__方法在語句體執行之前進入執行時上下文,__exit__在語句體執行完後從執行時上下文退出。

在實際應用中,__enter__一般用於資源分配,如開啟檔案、連線資料庫、獲取執行緒鎖;__exit__一般用於資源釋放,如關閉檔案、關閉資料庫連線、釋放執行緒鎖。

自定義上下文管理器

既然上下文管理器就是實現了__enter__和__exit__方法的物件,我們能不能定義自己的上下文管理器呢?答案是肯定的。

我們先來看下__enter__和__exit__方法的定義:

__enter__() - 進入上下文管理器的執行時上下文,在語句體執行前呼叫。如果有as子句,with語句將該方法的返回值賦值給 as 子句中的 target。

__exit__(exception_type, exception_value, traceback) - 退出與上下文管理器相關的執行時上下文,返回一個布林值表示是否對發生的異常進行處理。如果with語句體中沒有異常發生,則__exit__的3個引數都為None,即呼叫 __exit__(None, None, None),並且__exit__的返回值直接被忽略。如果有發生異常,則使用 sys.exc_info 得到的異常資訊為引數呼叫__exit__(exception_type, exception_value, traceback)。出現異常時,如果__exit__(exception_type, exception_value, traceback)返回 False,則會重新丟擲異常,讓with之外的語句邏輯來處理異常;如果返回 True,則忽略異常,不再對異常進行處理。

理解了__enter__和__exit__方法後,我們來自己定義一個簡單的上下文管理器。這裡不做實際的資源分配和釋放,而用列印語句來表明當前的操作。

bdc33759c5afbf96fce5168c44035db.png

執行上面的程式碼,會得到如下的輸出:

4c541d85b7e5abffe5b5e8fc05c3124.png

我們在with語句體中人為地丟擲一個異常:

5555ef5a677ca02b0a5abd09d364489.png

會得到如下的輸出:

8cb77267007f04c36a3f1ddb6f7bee7.png

如我們所期待,with語句體中丟擲異常,__exit__方法中exception_type不為None,__exit__方法返回False,異常被重新丟擲。

以上,我們透過實現__enter__和__exit__方法來實現了一個自定義的上下文管理器。

contextlib庫

除了上面的方法,我們也可以使用contextlib庫來自定義上下文管理器。如果用contextlib來實現,可以用下面的程式碼來實現類似的上下文管理器:

98520ebff8ddca6474b88ba5eeb7a2f.png

上面的程式碼涉及到裝飾器(@contextmanager),生成器(yield),有點難讀。這裡yield之前的程式碼相當於__enter__方法,在進入with語句體之前執行,yield之後的程式碼相當於__exit__方法,在退出with語句體的時候執行。

Python面試(七)你真的理解finally了嗎?

無論try語句中是否丟擲異常,finally中的語句一定會被執行。我們來看下面的例子:

45a23f4325d803a57e96ab06c16fcd5.png

不論try中寫檔案的過程中是否有異常,finally中關閉檔案的操作一定會執行。由於finally的這個特性,finally經常被用來做一些清理工作。

我們再來看下面的例子:

ac9cf324d4184765e41b45d25ca6020.png

這個例子中 func1() 和 func2() 返回什麼呢?

答案是 func1() 返回2, func2() 返回3。為什麼是這樣的呢?我們先來看一段Python官網上對於finally的解釋:

A finally clause is always executed before leaving the try statement, whether an exception has occurred or not. When an exception has occurred in the try clause and has not been handled by an except clause (or it has occurred in a except or else clause), it is re-raised after the finally clause has been executed. The finally clause is also executed “on the way out” when any other clause of the try statement is left via a break, continue or return statement.

重點部分用粗體標出了,翻成中文就是try塊中包含break、continue或者return語句的,在離開try塊之前,finally中的語句也會被執行。

所以在上面的例子中,func1() 中,在try塊return之前,會執行finally中的語句,try中的return被忽略了,最終返回的值是finally中return的值。func2() 中,try塊中丟擲異常,被except捕獲,在except塊return之前,執行finally中的語句,except中的return被忽略,最終返回的值是finally中return的值。

我們在上面的例子中加入print語句,可以更清楚地看到過程:

46ef73092a87e877afa80a93dfb844e.png

上面的程式碼輸出:

7f71675c939509e8cad651ec015632f.png

我們對上面的func2做一些修改,如下:

cec593df6ddcb582be814d209fb8225.png

輸出如下:

847fe8995d6cce64780c72f1ac0aebc.png

try中丟擲的異常是ValueError型別的,而except中定位的是IndexError型別的,try中丟擲的異常沒有被捕獲到,所以except中的語句沒有被執行,但不論異常有沒有被捕獲,finally還是會執行,最終函式返回了finally中的返回值3。

這裡還可以看到另外一個問題。try中丟擲的異常沒有被捕獲到,按理說當finally執行完畢後,應該被再次丟擲,但finally裡執行了return,導致異常被丟失。

可以看到在finally中使用return會導致很多問題。實際應用中,不推薦在finally中使用return返回。

python學習網,大量的免費,歡迎線上學習!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2041/viewspace-2835664/,如需轉載,請註明出處,否則將追究法律責任。

相關文章