回文章列表
Python7 分鐘閱讀2026年6月14日

Python try/except 入門:讓使用者亂打字也不會弄垮你的程式

#Python#try except#例外處理#防呆#新手

文章目錄

前面兩篇都刻意留了同一個坑沒補。

商品管理程式那篇的選單,只要在「請輸入功能選項」打一個非數字,整支程式立刻中止;串列統計那篇也一樣,成績改成從鍵盤輸入就會遇到同樣的問題。當時說「防呆是另一個獨立主題,之後再補」——這篇就是來補的。

防呆不是寫得多小心就好,它有固定的套路:接住錯誤 → 抓對類型 → 讓使用者重來。三步做完,程式才算能交到別人手上。

崩潰現場:int(input()) 為什麼會炸

先看一次完整的崩潰。這支 menu.py 只有一行關鍵程式:

choice = int(input('請輸入功能選項:'))
print(f'你選了 {choice}')

執行後在提示字元打 abc:

請輸入功能選項:abc
Traceback (most recent call last):
  File "menu.py", line 1, in <module>
    choice = int(input('請輸入功能選項:'))
             ~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'abc'

拆開來看這三個資訊,之後你除錯會快很多:

  • Traceback 是「回溯」,Python 在告訴你出事的位置。line 1 就是出問題那一行,底下那排 ~~~^^^ 更精準地指出是 int(...) 這段炸的,不是 input(...)。
  • ValueError 是錯誤的「類型」。這個名字很重要,等一下要用它來指定接住哪一種錯誤。
  • invalid literal for int() with base 10: 'abc' 是錯誤訊息,直譯是「abc 不是合法的十進位整數字面值」。

關鍵在於:input() 拿到的永遠是字串,就算你打 5 拿到的也是 '5' 而不是 5。int() 負責把字串轉成整數,轉得動就成功,轉不動就丟出 ValueError。而 Python 的例外只要沒人接,就會一路往外拋,最後由直譯器接手——印出 traceback、然後結束程式。

所以問題不是「使用者亂打字」,是我們沒有安排任何人去接這個錯誤。

實跑:輸入 abc 之後,連「程式正常結束」都看不到

下面用預先餵入 abc 的方式把課堂範例實跑一次(輸入以管線餵入,所以終端不會顯示你打的字):

age = int(input("年齡:"))
print(f"明年 {age + 1} 歲")
print("程式正常結束")

執行結果:

年齡:Traceback (most recent call last):
  File "C:\demo\demo.py", line 1, in <module>
    age = int(input("年齡:"))
          ~~~^^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'abc'
執行結果:輸入 abc 後 ValueError,最後一行 print 沒有被執行
執行結果:輸入 abc 後 ValueError,最後一行 print 沒有被執行

讀錯誤訊息的順序:由下往上看。最後一行告訴你「錯誤種類+原因」,往上一行 File ... line 1 告訴你「第幾行」。注意 age + 1 與最後的 print 都沒有執行——例外一旦沒被接住,整支程式就在那一行停止。(Python 3.11 之後的錯誤訊息會多出 ~~~^^^ 標示錯誤位置,舊版沒有,版本不同箭頭符號會略有差異。)

寫例外之前,我會先讓程式自己報錯

怎麼知道一段程式會發生哪些例外?我教學生的最好方式是:先不要寫 try/except,讓程式自己引發錯誤,看清楚它丟出的是哪個例外類別,再來紀錄和撰寫。這樣不但寫得準,也能清楚知道這段程式會發生哪些例外。

學生最常遇到的,是輸入與運算的時候:轉型失敗、除以零、陣列(串列)取超過位置。另外像爬蟲,在取得遠端資料、解析資料的時候也都可能出問題,這種情況要把不同的例外分開寫好,才知道問題到底出在哪一塊。

try / except:把錯誤接住

try / except 的語意很直白:「試著執行 try 裡的程式;如果中途丟出例外,就跳到 except 去處理,不要讓程式死掉」。

try:
    choice = int(input('請輸入功能選項:'))
    print(f'你選了 {choice}')
except ValueError:
    print('請輸入數字!')

一樣打 abc:

請輸入功能選項:abc
請輸入數字!

程式沒有崩潰,印出一句人看得懂的話就繼續往下走。

有兩個細節值得注意。第一,try 區塊裡出錯的那一行之後的程式不會執行——上面那個 print(f'你選了 {choice}') 完全沒印出來,因為程式在前一行就跳到 except 了。第二,except ValueError 後面接的是錯誤類型的名稱,就是剛才 traceback 最後一行看到的那個字。這不是巧合,Python 的錯誤類型本來就是設計來讓你指名接住的。

try 區塊要盡量只包「可能出錯的那幾行」。包太大範圍的話,你會分不清楚到底是哪一行出的問題,反而更難除錯。

別寫裸 except:——要抓對錯誤類型

新手常見的偷懶寫法是不指定類型,直接寫 except:。這樣確實什麼都接得住,但那正是問題所在。

先看為什麼需要「分類接」:

try:
    n = int(input('請輸入除數:'))
    print(100 / n)
except ValueError:
    print('請輸入數字!')
except ZeroDivisionError:
    print('除數不能是 0!')

輸入 0:

請輸入除數:0
除數不能是 0!

同一段程式有兩種出錯的方式——打字打錯是 ValueError,輸入 0 是 ZeroDivisionError——各自對應到不同的提示。如果混成一句「輸入有誤」,使用者根本不知道該怎麼改。except 可以疊好幾層,Python 會由上往下比對,用第一個對得上的。

那裸 except: 到底錯在哪?它會連你自己寫錯的 bug 一起吞掉。假設你把變數名稱打錯成 nubmer,正常會丟 NameError 讓你當場發現;被裸 except: 接住之後,程式只會印「請輸入數字!」然後若無其事地跑下去,你會對著一個永遠說「請輸入數字」的程式研究半天。它甚至會接住使用者按 Ctrl+C 想中斷程式的訊號,讓程式關不掉。

規則很單純:你預期會發生、而且知道怎麼處理的錯誤,才寫進 except。沒預期到的錯誤就讓它炸出來——那個 traceback 是在幫你,不是在找你麻煩。

裸 except: 會把你自己的打字錯誤也吞掉

程式出問題的原因不一定是使用者輸入。下面故意把 price 少打一個 e:

price = 30
quantity = 5

try:
    total = pric * quantity      # 少打一個 e
    print("金額:", total)
except:
    print("輸入錯誤,請重新輸入")

執行結果:

輸入錯誤,請重新輸入

畫面只說「輸入錯誤」,但這次根本沒有人輸入任何東西,真正的原因是變數名稱打錯,這個訊息會讓你去錯誤的地方找很久。把 except: 改成只接預期的錯誤 except ValueError:,同一支程式的行為變成:

Traceback (most recent call last):
  File "C:\demo\demo.py", line 5, in <module>
    total = pric * quantity      # 少打一個 e
            ^^^^
NameError: name 'pric' is not defined. Did you mean: 'price'?
執行結果:改成 except ValueError 之後 NameError 現形,Python 還提示 Did you mean: price?
執行結果:改成 except ValueError 之後 NameError 現形,Python 還提示 Did you mean: price?

這就是「抓對類型」的價值:預期內的錯誤(使用者輸入不是數字)你處理;預期外的錯誤(你自己的 bug)讓它大聲報出來。

為什麼要把錯誤分類

裸 except: 看似省事,因為全部接起來就不會崩潰。但我的看法是:如果所有錯誤都指向同一個說明,最後客戶回報問題時,連我們自己也搞不清楚錯誤的根源。不如早點把 except 的類別分好,客服和後續修改的人都輕鬆很多。

分好類還有一個好處:可以針對不同的錯誤,產出不同的 LOG 報表,之後要統計哪一類錯誤最常發生就有依據。

多個 except 的順序

except 是由上往下依序比對,第一個符合的就執行。把範圍大的 Exception 寫在前面,後面的 ValueError 就永遠輪不到——而且 Python 不會警告你:

try:
    n = int("abc")
except Exception:
    print("發生錯誤")
except ValueError:
    print("轉型失敗(想讓使用者看到這行,但永遠不會出現)")

執行結果:

發生錯誤

int("abc") 明明丟的是 ValueError,卻印出籠統的「發生錯誤」,我們想給使用者看的「轉型失敗」那行永遠不會出現。原則:子類別(具體)在前、父類別(籠統)在後。

配上 while,問到對為止

接住錯誤只是不崩潰,但使用者要的功能還是沒完成。真正堪用的防呆要能讓他重來,這就得靠 while 迴圈:

while True:
    try:
        choice = int(input('請輸入功能選項(1-5):'))
    except ValueError:
        print('那不是數字,請重新輸入')
        continue
    if 1 <= choice <= 5:
        break
    print('請輸入 1 到 5 之間的數字')

print(f'收到選項 {choice}')

依序輸入 abc、99、3:

請輸入功能選項(1-5):abc
那不是數字,請重新輸入
請輸入功能選項(1-5):99
請輸入 1 到 5 之間的數字
請輸入功能選項(1-5):3
收到選項 3

這個結構檢查了兩件不同的事,順序不能顛倒:先確認轉得成數字(try),再確認數字在合理範圍(if)。99 是完全合法的整數,int() 轉得過去、try 也不會有意見,但它不是選單上的選項,所以要另外用 if 擋。

continue 的作用是「這一輪不用再往下跑了,直接回到迴圈開頭重問」。少了它,程式會帶著一個沒有成功賦值的 choice 繼續執行下面的 if,反而引發新的錯誤。

1 <= choice <= 5 是 Python 特有的連續比較寫法,等同於 choice >= 1 and choice <= 5,但更接近數學上的直覺。

裝回商品管理程式

回到最初那支水果商品管理程式。把上面那段防呆抽成一個函式,選單就乾淨了:

def ask_choice():
    while True:
        try:
            n = int(input('請輸入功能選項:'))
        except ValueError:
            print('請輸入數字 1-5')
            continue
        if 1 <= n <= 5:
            return n
        print('沒有這個選項,請輸入 1-5')


fruitPrices = {}
while True:
    print('{:*^20}'.format('水果商品管理程式'))
    choice = ask_choice()
    if choice == 5:
        print('離開程式')
        break
    elif choice == 1:
        item = input('請輸入新產品:')
        if item in fruitPrices:
            print('已有該商品!')
        else:
            price = input(f'請輸入 {item} 價格:')
            fruitPrices[item] = price
            print(f'產品 {item} 新增完成!')
    elif choice == 4:
        print('目前產品:')
        for key, value in fruitPrices.items():
            print(f'{key}={value}', end=' ')
        print()

(為了聚焦在防呆,這裡只留新增與查詢兩個選項,刪除與修改照原文補回去即可。)

實際跑一輪,中間故意打一個 x:

******水果商品管理程式******
請輸入功能選項:1
請輸入新產品:蘋果
請輸入 蘋果 價格:30
產品 蘋果 新增完成!
******水果商品管理程式******
請輸入功能選項:x
請輸入數字 1-5
請輸入功能選項:4
目前產品:
蘋果=30 
******水果商品管理程式******
請輸入功能選項:5
離開程式

打錯字的那一輪只是被退回重問,資料還在、程式還活著。

把防呆包成 ask_choice() 而不是塞在主迴圈裡,有個實際的好處:主迴圈只剩下業務邏輯,一眼就看得出這支程式在做什麼;而「怎麼確保拿到合法選項」是另一件事,被關在函式裡不干擾閱讀。之後你要加「輸入價格也要防呆」,就再寫一個 ask_price(),同樣的套路再來一次——try 接住轉型失敗、while 讓他重來、if 擋掉不合理的值。

這三步幾乎可以套用在任何需要讀取使用者輸入的程式上。學會之後,你寫的東西才算真的能交給別人用,而不是只有自己小心翼翼地照著正確順序操作才不會壞。

工作上的兩個例子:例外處理沒寫會怎樣

我在實務上遇過兩次例外處理沒做好的後果:

  • UI 面板上的數值輸入框:使用者輸入錯誤,轉換沒有處理好,程式跳出不預期的錯誤,整個當機。這和上面 int(input()) 輸入 abc 是同一件事,只是場景換成了操作面板。
  • 重複批次操作指令:其中一次網路斷線,整批指令就報銷。加上 except 之後,可以跳過那一次,並且做重連測試,不用整批重來。

所以我跟剛學的人說:例外處理不是為了讓畫面好看,是為了讓程式在別人手上遇到意外時,不會整個倒掉。

相關課程

Python 基礎入門

從零開始學習 Python 程式語言,掌握變數、流程控制、函式與模組化設計,建立扎實的程式設計基礎。

查看課程內容