Python :淺析 return 和 finally 共同挖的坑

發表於2017-08-25

初識 return

相信每一個用過Python函式的童鞋, 肯定會用過return語句, return顧名思義, 就是用來返回值給呼叫者, 例如:

對於上面的結果, 相信大家都不會感到意外, 那麼加大點難度, 如果在return語句還有程式碼呢? 那句程式碼會怎樣呢?

老司機肯定一眼就能看出結果, 但是對於尚在入門或者對return不很瞭解的童鞋, 可能就會懵逼了~ 後面的兩句程式碼是否會被執行?

答案是: 不會執行

return正如它的名字那樣, 當執行這句程式碼, 整個函式都會返回, 整個呼叫就算結束了~ 所以在return後面的程式碼, 都是不會被執行的!

也正因為這個特性, 所以有種編碼規範叫early return的編碼規範就被倡導。

它的意思大概就是: 當條件已經滿足返回時, 就馬上返回

舉個例子來說明:

上面的程式碼應該比較容易理解, 就是根據a的值, 來決定返回的result是什麼. 這樣的編碼相信也是大部分童鞋喜歡用的, 因為這樣比較符合我們直覺, 然而, 這樣寫似乎有點浪費, 因為當第一個判斷結束了, 如果結果為真, 就應該返回more than, 然後結束函式, 否則肯定就是返回less than, 所以我們可以把程式碼調整成這樣:

甚至是:

結果都是和第一個寫法是一樣的! 第一次看到這樣寫法的童鞋, 可能會覺得比較難以接受, 甚至覺得可讀性很差, 但是其實這樣的寫法, 我覺得反而會稍微好點. 因為:

  1. 執行的程式碼數少了, 呼叫方能更快得到結果
  2. 有利於減少巢狀的層數, 便於理解.

對於第2點在這需要解釋下, 很多時候我們寫得程式碼, 巢狀很深, 都是因為if/else的鍋, 因為巢狀的if/else 比較多, 所以導致一堆程式碼都巢狀得比較深, 這樣對於其他小夥伴, 簡直就是災難, 因為他們很可能在閱讀這部分程式碼時, 就忘了前面的邏輯….
為了更加容易理解, 舉個程式碼例子:

程式碼簡化優化版:

這樣對比這來看, 應該能更好地理解為什麼說early return能夠減少巢狀的層數吧~ 有疑問歡迎留言討論~

談談深坑

剛才花了比較長的篇幅去介紹return, 相信看到這裡, 對於return應該有比較基本的理解了! 所以來聊聊更加迷惑的話題:

如果剛才有認真看的話, 會注意到一句話, 就是:

但事實真的這樣嗎? 通常這樣問, 答案一般都不是 ~~
先來看看例子:

可以猜猜這句print a會不會列印? 相信很多童鞋都想了一會, 然後說不會~ 然而這個答案是錯的, 真正的輸出是:

有木有覺得彷彿看見了新大陸, 在一開始的例子中, return後面的語句沒有被執行, 但是在這裡, 相隔那麼遠, 卻依舊沒有忘記, 這或許就是”真愛”吧!

然而就是因為這種”真愛”, 總是會讓一堆新老司機掉坑裡..然後還不知道為毛..

為了避免它們再繼續借用打著”真愛”的幌子, 欺負我們, 讓我們一起來揭開這”真愛”的真面目!

於是, 我們得藉助偷窺神器: dis, 想想都有點小興奮!

輸出比較長, 單獨寫:

這邊簡單說著這些列所代表的意思:

在位元組碼中可以看到, 依次是SETUP_FINALLYSETUP_EXCEPT, 這個對應的就是finallytry,雖然finallytry後面, 雖然我們通常幫他們看成一個整體, 但是他們在實際上卻是分開的… 因為我們重點是finally, 所以就單單看SETUP_FINALLY

從上面的程式碼, 很明顯就能看出來, SETUP_FINALLY 就是呼叫下PyFrame_BlockSetup去建立一個Block, 然後為這個Block設定:

  1. b_type (opcode 也就是SETUP_FINALLY)
  2. b_level
  3. b_handler (INSTR_OFFSET() + oparg)

handler 可能比較難理解, 其實看剛才的 dis 輸出就能看到是哪個, 就是 13 >> 31 LOAD_CONST 2 (‘finally’), 這個箭頭就是告訴我們跳轉的位置的, 為什麼會跳轉到這句呢? 因為6 0 SETUP_FINALLY 28 (to 31)已經告訴我們將要跳轉到31這個位置~~~

如果這個搞清楚了, 那就再來繼續看 return, return對應的位元組碼是: RETURN_VALUE, 所以它對應的原始碼是:

原來我們以前理解的return是假return! 這個return並沒有直接返回嘛, 而是將堆疊的值彈出來, 賦值個retval, 然後將why設定成WHY_RETURN, 接著就跑路了! 跑到一個叫fast_block_end;的地方~, 沒辦法, 為了揭穿真面目, 只好掘地三尺了:

在這需要回顧下剛才的一些知識, 剛才我們看了return的程式碼, 看到它將why設定成了 WHY_RETURN, 所以在這麼一大串判斷中, 它只是走了最後面的else, 動作也很簡單, 就是將剛才return儲存的值retvalpush壓回棧, 同時將why轉換成long再壓回棧, 然後有設定了下why,接著就是屁顛屁顛去執行剛才SETUP_FINALLY設定的b_handler程式碼了~ 當這這段bhandler程式碼執行完, 就再通過END_FINALLY去做回該做的事, 而這裡就是, return retval

結論

所以, 我們應該能知道為什麼當我們執行了return程式碼, 為什麼finally的程式碼還會先執行了吧, 因為return的本質, 就是設定whyretval, 然後goto到一個大判斷, 最後根據why的值去執行對應的操作! 所以可以說並不是真的實質性的返回. 希望我們往後再用到它們的時候, 別再掉坑裡!

相關文章