Day 09 例外處理、遞迴:誰用三生浮世的煙火,換你一次長憶的交錯
註:本篇文章同步刊載於iT邦幫忙,為鐵人賽之系列文章。
https://ithelp.ithome.com.tw/articles/10242043
先來解答昨天的問題吧!
- 為了表現出串列表達式的用法,
這邊我們就先不將range的部分和取偶數合併,
不然使用range(2, 11, 2)就行了。
連同2的寫法如下:
| |
執行結果參考如下:
| |
- 以現有給出來的內容,我們可以寫成類似以下的小遊戲:
| |
請留意,這只是按照我們現在已經學到的部分來撰寫的,
以這個解法來說的話就包含了以下幾個明顯的bug
(程式臭蟲,指程式碼中有一些問題導致運行會當掉或是輸出結果有問題,
可能是輸入錯誤,也有可能是程式的邏輯本身出的問題):
- 輸入非數字的時候會因為無法轉換成整數而使程式當掉。
- 輸入數字可以限縮範圍,但我們並沒有真的限制使用者只能輸入這個範圍內的數字。
請參照執行結果,應該能理解上面兩點所造成的問題。
| |
| |
| |
接下來我們來講今天的第一個主題:
如昨天的第三題來說,我們會發現使用者永遠可以超乎你的想像。
這時候你希望得到可以轉成1~100的字串,並且能夠處理掉輸入錯誤的問題的話,
該怎麼辦呢?
如果你能將使用者輸入的每一種內容都分門別類,
當然可以用if…elif…else的形式來進行解決,
如果沒有想要分那麼精細呢?
我們可以使用Python的錯誤處理機制try…except…。
基本語法如下:
| |
讓我們改一下前面的程式碼看看:
| |
在上面的程式中,當我們用try包住input那行以後,
一旦我們輸入了不是數字的東西,原本Python會報錯並結束程式,
此時就會改成來到except處來執行對應的程式碼。
(如果沒輸入錯誤,except的部分就會被忽略)
事實上,不同的例外在Python當中會有不同的錯誤類型,
如果你想要分開精細地處理不同類型的例外的話,
就要寫成如下型式:
| |
當一個例外發生時會依序由上到下檢查符合哪一個種類的例外,
先滿足條件的獨佔(可以想成類似if…elif的檢查方式)
例外的類別有像是UppercaseException, IndexError, ValueError等等,
他們的共同的大類別都是Exception。
你也可以使用raise Exception(傳入值)來主動將例外丟出。
(如果你有做好處理的準備的話)
同時,它可以帶著給定的訊息,有助於我們判斷到底是怎麼一回事。
| |
接下來我們要來講一個日後各位一定碰得到也很常用的方法:遞迴(Recursion) 。
什麼是遞迴呢?
遞迴就是當一個函式在使用時,中途不斷呼叫自己,
藉以達到某些目的或完成某些問題。
通常狀況下,
寫得正常的遞迴應該會不斷在過程中將要計算的問題進行簡化,
最終只剩下我們要的東西,同時沒有再繼續呼叫函式。
聽起來……超級無敵抽象的阿!!!
沒關係,讓我們看看實際的範例。
還記得那個白目的小男孩高斯嗎?
假設我們不會公式,希望使用Python來幫我們手算1~100,
按現有前面學過的東西,你可能會這樣寫:
| |
| |
如果你不會range的話可能就真的要一個一個加了!
那麼,從遞迴的角度是怎麼看待這件事情的呢?
答案是一個一個處理 。
既然cal(100)是1加到100,那麼cal(99)就是1加到99;
因此,cal(100)就相當於100加上cal(99)囉!
以此類推,我們可以得到cal(end)會等於end + cal(end — 1)。
於是,我們可以將函式按照這個概念重寫成下面的樣子:
| |
有沒有覺得哪邊怪怪的?
我們忘記一件事情,就是當碰到cal(1)的時候,
它本身應該要直接等於1才對,
不然按照這樣呼叫下去,cal輸入的值會變成0, -1, -2, -3, … ,
但不會停下來XD!
那我們再改寫一下:
| |
說明一下,上面的式子前面還沒有講過,但應該不難理解,
這是屬於Python的三元運算子,使用if跟else,
也就是說,當end != 1時,這行會回傳前面那段”end + cal(end — 1)” ,
否則,就回傳1 (也就是當end為1時,直接回傳1就對了!)。
實際在呼叫函式的時候,例如將100代入,
我們會先呼叫cal(100),發現cal(100)要算100+cal(99);
Python會將100先記錄下來,先去算cal(99),
也就是等算出cal(99)後,再回過頭來加上100;
cal(99)又會被拆成99+cal(98),以此類推。
由於函式執行時實際上需要記憶體空間,這種還沒得到結果的函式,
往下又呼叫了東西的話,就會再疊上下一段的函式,
也就是在電腦的記憶體中會有地方放cal(100), cal(99), cal(98)… ,
一路堆疊上去,這樣子的結構被我們稱之為呼叫堆疊(call stack) 。
所以假設一個函式還沒完成,
就又在裡面呼叫另一個函式(可能是自己或別人)的話,
call stack就會越疊越高,從而占用大量記憶體。
因此,一般來說程式語言都會限制call stack最大的堆疊的函式數量,
以Python來說,可以用以下的方式來檢查call stack的限制:
| |
所以如果我們將剛剛的cal改成1000的話,這個程式就廢了XD!
| |
我們總結一下建立一個遞迴函式的條件:
- 我們可以將一個要計算的東西拆小 ,不管是拆成一部分已知一部分未知 ,
或是拆成數塊比較小的部分 都可以。 - 在越拆分越小的時候,要至少有一個狀態是可以直接得到結果的 (比如剛剛的cal(1)=1),
因而,讓所有的區塊最後都能變成已知,從而可以計算出結果 。
讀者可能會有一些疑惑:
看起來遞迴好像就簡單一點,但是沒有迴圏快阿!
而且迴圏也不會有限制call stack的層數的問題,為什麼不用迴圏呢?
好問題!
我們現在示範的只是很簡單的範例,
事實上在絕大多數狀況下,可以用遞迴的問題,用迴圏也寫得出來,
這邊通常會稱使用迴圏寫出來的解法為迭代解(iterative solution)。
問題是……
越難的題目,通常遞迴解就會很常比迭代解還要容易思考,
而迭代解有時候會需要用到比較複雜的推導,才能夠寫出來!
所以如果未來讀者在學了Python以後,
可能還學了一些別的進階的應用,當面試的時候被考到白板題,
在沒有特別的狀況下,請盡量先確定給出自己遞迴解,
有信心的話,再更進一步去嘗試迭代解,會比較穩妥。
補充:
感謝FB 程式人雜誌 社團的網友** Cheng-En Chuang**留言補充如下,
想更深入了解遞迴的運作的話,請參考他的解說及提供的參考資料:
「有點雞蛋裡挑骨頭,
但遞迴在Python裡面有深度限制是因為沒做tail-call optimization。
在其他有做tail-call optimization的語言裡面就不會有深度限制,
另外在某些語言裡面recursion也不一定比iteration慢。
甚至在Python裡面也可以用decorator達到tail-call效果:
https://towardsdatascience.com/python-stack-frames-and-tail-call-optimization-4d0ea55b0542」
那我們一樣來練習題目吧!
- 假定有一個樓梯,你從第0階要爬到第n階,
每次你只能選擇爬1階或者爬2階,這樣稱做一步。
請寫出一個函式名為cs,給定n的値以後(n > 0),
計算出從第0階爬到第n階的方法共有幾種不同的變化?
例:
cs(1) = 1 (1)
cs(2) = 2 (1+1, 2)
cs(3) = 3 (1+2, 2+1, 1+1+1)
cs(4) = 5 (1+1+2, 2+2, 1+2+1, 2+1+1, 1+1+1+1)
請分別給出遞迴解和迭代解。
對初學者來說,可能不是那麼容意思考,請務必好好觀察函式之間的關係。
辛苦啦!我們明天見!
工商時間:
抽獎活動還在繼續累積人數(現在好像沒有人想抽XD)
在Python Taiwan的連結第100篇的文章 底下,
公開分享到你的臉書、按讚該篇文章、並留言告訴我說,
「你最喜歡這一整個系列的哪一篇?為什麼?」或
「除了從LeetCode學演算法系列以外,
你還想要看到關於什麼方向的文章?」
超過20則留言的話 (有完成以上步驟的才算),我們就抽一組
「從Leetcode學演算法|進階篇」+「從Leetcode學演算法|面試篇」
課程的免費兌換券進行贈送!
期限嘛…就延長到滿人數吧XDD (不然也沒辦法哈哈)
容筆者工商一下,
「從Leetcode學演算法|進階篇」 開放預購啦!
這次選了40道難度加深的LeetCode題目,
同樣也會細部解說對應的技巧及須要掌握的演算法!
同時這次購買進階篇的話,
額外還加贈**「從Leetcode學演算法|面試篇」** !
當中包含了面試準備須知分享 ,及訪談國內外不同經驗的工程師 ,
讓你不論是想走前端/後端/一般軟工 或者是想找國外的工作 ,
是初學想轉職 還是正在工作 ,都能夠從中得到收穫呦!
有興趣的朋友可以使用下面的早鳥優惠~
「從Leetcode學演算法|進階篇」+「從Leetcode學演算法|面試篇」 :
https://bit.ly/advleetcode
「從Leetcode學演算法」全套(基礎/進階/面試篇)同捆優惠:
https://bit.ly/allleetcode
