前面兩篇都刻意留了同一個坑沒補。
商品管理程式那篇的選單,只要在「請輸入功能選項」打一個非數字,整支程式立刻中止;串列統計那篇也一樣,成績改成從鍵盤輸入就會遇到同樣的問題。當時說「防呆是另一個獨立主題,之後再補」——這篇就是來補的。
防呆不是寫得多小心就好,它有固定的套路:接住錯誤 → 抓對類型 → 讓使用者重來。三步做完,程式才算能交到別人手上。
崩潰現場: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、然後結束程式。
所以問題不是「使用者亂打字」,是我們沒有安排任何人去接這個錯誤。
try / except:把錯誤接住
try / except 的語意很直白:「試著執行 try 裡的程式;如果中途丟出例外,就跳到 except 去處理,不要讓程式死掉」。
try:
choice = int(input('請輸入功能選項:'))
print(f'你選了 {choice}')
except ValueError:
print('請輸入數字!')一樣打 abc:
請輸入功能選項:abc
請輸入數字!程式沒有崩潰,印出一句人看得懂的話就繼續往下走。
有兩個細節值得注意。第一,try 區塊裡出錯的那一行之後的程式不會執行——上面那個 print(f'你選了 {choice}') 完全沒印出來,因為程式在前一行就跳到 了。第二, 後面接的是,就是剛才 traceback 最後一行看到的那個字。這不是巧合,Python 的錯誤類型本來就是設計來讓你指名接住的。
別寫裸 except:——要抓對錯誤類型
新手常見的偷懶寫法是不指定類型,直接寫 except:。這樣確實什麼都接得住,但那正是問題所在。
先看為什麼需要「分類接」:
try:
n = int(input('請輸入除數:'))
print(100 / n)
except ValueError:
print('請輸入數字!')
except ZeroDivisionError:
print('除數不能是 0!')輸入 0:
請輸入除數:0
除數不能是 0!同一段程式有兩種出錯的方式——打字打錯是 ValueError,輸入 0 是 ZeroDivisionError——各自對應到不同的提示。如果混成一句「輸入有誤」,使用者根本不知道該怎麼改。 可以疊好幾層,Python 會由上往下比對,用第一個對得上的。
配上 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:
裝回商品管理程式
回到最初那支水果商品管理程式。把上面那段防呆抽成一個函式,選單就乾淨了:
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()