如果你已經學過 dict 的基本操作——d[key] = value 存資料、for k, v in d.items() 走訪、in 判斷 key 存不存在——大概會有一個疑問:這些零散的操作要怎麼兜成一支「真正的程式」?
實務上答案很單純:套一個 while True 選單,把 dict 的增刪改查各包成一個選項,使用者輸入數字選功能,程式就一直跑到使用者選擇離開為止。這篇就帶你寫一支完整的「水果商品管理程式」,把 dict 的 CRUD(新增 Create、查詢 Read、修改 Update、刪除 Delete)全部串起來。
先看完整程式長什麼樣子
這支程式用一個 fruitPrices 字典存商品名稱和價格,選單有五個選項:
fruitPrices = {}
while True:
print('{:*^20}'.format('水果商品管理程式'))
print('{:^20}'.format('(1).新增產品'))
print('{:^20}'.format('(2).刪除產品'))
print('{:^20}'.format('(3).修改產品'))
print('{:^20}'.format('(4).目前產品'))
print('{:^20}'.format('(5).離開'))
choice = int(input('請輸入功能選項:'))
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 == 2: # 刪除
item = input('請輸入刪除產品:')
if item not in fruitPrices:
print('無該商品')
else:
del fruitPrices[item]
print(f'產品 {item} 刪除完成!')
elif choice == 3: # 修改
item = input('請輸入修改產品:')
if item not in fruitPrices:
print('無該商品')
else:
fruitPrices[item] = input(f'請輸入 {item} 價格:')
print(f'產品 {item} 修改完成!')
elif choice == 4: # 目前產品
print('目前產品:')
for key, value in fruitPrices.items():
print(f'{key}={value}', end=' ')
print()先不急著逐行看,我們一段一段拆開講。
第一步:選單骨架,先把「離開」寫對
任何選單程式的骨架都長這樣:while True 讓程式不斷重複印選單、讀輸入,只有輸入符合離開條件時才 break 跳出迴圈。
fruitPrices = {}
while True:
print('{:*^20}'.format('水果商品管理程式'))
print('{:^20}'.format('(1).新增產品'))
print('{:^20}'.format('(2).刪除產品'))
print('{:^20}'.format('(3).修改產品'))
print('{:^20}'.format('(4).目前產品'))
print('{:^20}'.format('(5).離開'))
choice = int(input('請輸入功能選項:'))
if choice == 5:
print('離開程式')
break這裡有兩個 str.format() 的置中用法值得看一下:
'{:*^20}'.format('水果商品管理程式'):^是置中對齊,20是總寬度,*是補在兩側的填充字元。跑出來會是******水果商品管理程式******(星號補滿兩側湊到 20 個字元寬)。'{:^20}'.format('(1).新增產品'):同樣是置中,但沒指定填充字元,預設用空白補,讓每個選項看起來左右對齊、排版工整。
先把這段單獨跑起來,你會看到選單一直重複印,直到輸入 5 才會印出「離開程式」然後結束。這一步先確定選單骨架沒問題,之後加其他功能才不會混在一起除錯。
第二步:新增與刪除——in / not in 用法剛好相反
接下來把「新增」「刪除」接在 choice == 5 判斷式之後,用 elif 串起來:
elif choice == 1: # 新增
item = input('請輸入新產品:')
if item in fruitPrices:
print('已有該商品!')
else:
price = input(f'請輸入 {item} 價格:')
fruitPrices[item] = price
print(f'產品 {item} 新增完成!')
elif choice == 2: # 刪除
item = input('請輸入刪除產品:')
if item not in fruitPrices:
print('無該商品')
else:
del fruitPrices[item]
print(f'產品 {item} 刪除完成!')這段是整支程式最該記住的地方:新增前檢查「已存在」,刪除前檢查「不存在」,方向剛好相反。
- 新增的邏輯是「不能加進已經有的東西」,所以用
if item in fruitPrices擋掉重複新增。 - 刪除的邏輯是「不能刪不存在的東西」,所以用
if item not in fruitPrices擋掉刪除失敗。
in 和 not in 對 dict 做的事情,是拿左邊的值去比對 dict 所有的 key(不會比對 value)。item in fruitPrices 等於在問「item 是不是 fruitPrices 的某個 key」。
真正執行刪除動作的是 del fruitPrices[item]——del 是 Python 的關鍵字,作用是把整個 key-value pair 從字典裡移除,不是只把 value 清空。而新增動作反過來很單純,直接賦值即可:fruitPrices[item] = price,如果這個 key 原本不存在,Python 會自動幫你建立。
第三步:修改與查詢——一個是「有條件的賦值」,一個是走訪
elif choice == 3: # 修改
item = input('請輸入修改產品:')
if item not in fruitPrices:
print('無該商品')
else:
fruitPrices[item] = input(f'請輸入 {item} 價格:')
print(f'產品 {item} 修改完成!')
elif choice == 4: # 目前產品
print('目前產品:')
for key, value in fruitPrices.items():
print(f'{key}={value}', end=' ')
print()「修改」跟「刪除」用同一種存在性檢查(not in,因為不能改一個不存在的商品),但真正動手的方式跟「新增」一樣,都是 fruitPrices[item] = 新值。這其實透露一個重點:Python 的 dict 沒有分開的「新增」和「修改」指令,dict[key] = value 這一行,key 不存在時是新增、key 存在時就是修改,差別只在於程式邏輯要不要先用 in/not in 攔一層。
「目前產品」則是單純走訪整個字典:fruitPrices.items() 會把每一組 key-value 拆成 (key, value) 的組合讓 for 迴圈取用,所以能寫成 for key, value in fruitPrices.items()。這裡用 print(f'{key}={value}', end=' ') 讓每個商品印在同一行、用空白隔開,迴圈跑完之後再補一個空的 print() 換行,不然下一輪選單會緊接在商品清單後面,看起來很擠。
dict[key] 與 .get() 的差別
fruitPrices = {"蘋果": "30"}
print(fruitPrices.get("香蕉", "無該商品"))
print(fruitPrices["香蕉"])執行結果:
無該商品
Traceback (most recent call last):
File "C:\demo\demo.py", line 3, in <module>
print(fruitPrices["香蕉"])
~~~~~~~~~~~^^^^^^^^
KeyError: '香蕉'![執行結果:.get() 查不到回傳預設值;用 [] 查不到會 KeyError](/blog/python-dict-product-manager/keyerror.png)
dict[key] 查不到會直接丟 KeyError(而且錯誤訊息只有 key 本身 '香蕉');.get(key, 預設值) 則溫和地回傳預設值。要用哪個取決於「查不到是正常情況還是 bug」:使用者查詢商品——正常,用 get 或 in;程式內部保證一定存在的設定——用 [],出錯就該大聲報。(Python 3.11 之後的錯誤訊息會多出 ~~~^^^ 標示錯誤位置,舊版沒有,版本不同箭頭符號會略有差異。)
合併起來跑一次
把三段接在一起就是文章開頭那支完整程式。實際操作一輪:先新增蘋果 30 元、香蕉 20 元,再查看目前產品,最後離開:
******水果商品管理程式******
(1).新增產品
(2).刪除產品
(3).修改產品
(4).目前產品
(5).離開
請輸入功能選項:1
請輸入新產品:蘋果
請輸入 蘋果 價格:30
產品 蘋果 新增完成!
******水果商品管理程式******
(1).新增產品
(2).刪除產品
(3).修改產品
(4).目前產品
(5).離開
請輸入功能選項:1
請輸入新產品:香蕉
請輸入 香蕉 價格:20
產品 香蕉 新增完成!
******水果商品管理程式******
(1).新增產品
(2).刪除產品
(3).修改產品
(4).目前產品
(5).離開
請輸入功能選項:4
目前產品:
蘋果=30 香蕉=20
******水果商品管理程式******
(1).新增產品
(2).刪除產品
(3).修改產品
(4).目前產品
(5).離開
請輸入功能選項:5
離開程式商品順序照新增順序排列(蘋果先加,所以先印),這是 Python 3.7 之後 dict 保留插入順序的特性,不是巧合。
實跑:新增、重複新增、列出、離開
為了讓讀者能自己驗證,這裡把課堂用的版本精簡成只保留新增/目前產品/離開(邏輯相同),並以預先準備好的輸入實跑。輸入順序:1→蘋果→30→1→蘋果(重複)→4→5。由於輸入以管線餵入,終端不會顯示打的字,請對照輸入順序閱讀:
fruitPrices = {}
while True:
print('{:*^20}'.format('水果商品管理程式'))
print('(1).新增產品 (4).目前產品 (5).離開')
choice = int(input('請輸入功能選項:'))
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()執行結果:
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:請輸入新產品:請輸入 蘋果 價格:產品 蘋果 新增完成!
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:請輸入新產品:已有該商品!
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:目前產品:
蘋果=30
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:離開程式
第二次新增蘋果時,程式印出「已有該商品!」並沒有覆蓋價格——這就是 if item in fruitPrices 的守門效果。
學生做這個作業最常見的四個錯
學生做這支選單/CRUD 小作業,我看過最常見的錯有四種,你自己寫完之後可以對照檢查:
1. 判斷輸入選項時,數字 1 和字串 "1" 搞混。 input() 拿到的是字串,沒轉型就拿去和數字 1 比,永遠不相等,選單怎麼選都沒反應。
2. 忘記先寫離開的邏輯。 選單是 while True,如果沒先把「離開」寫對,程式就很難停下來(第一步先寫離開,就是為了避免這件事)。
3. 例外處理沒用好,只要輸入錯誤,程式就崩潰。 下一段會實際看一次。
4. 無法連續輸入品項。 新增完一個商品就結束,沒辦法一次輸入好幾個,通常是迴圈放錯位置。
一個常見的坑:選單輸入打錯字會直接崩潰
試著在「請輸入功能選項:」故意打一個中文字或英文字看看,例如輸入 abc,你會看到程式直接丟出 ValueError: invalid literal for int() with base 10: 'abc' 然後整支程式當掉。
原因在這一行:
choice = int(input('請輸入功能選項:'))input() 拿到的永遠是字串,int() 硬把它轉成整數;只要字串裡不是純數字,int() 轉換失敗就會丟出 ValueError,而這支程式沒有做任何攔截,例外會一路往外拋,直接中止整個程式。
真正上線的程式通常會加一層 try/except 或先用 str.isdigit() 檢查再轉換,把這個崩潰點補起來。這篇先把 dict 的 CRUD 邏輯講清楚,所以前面的版本是刻意簡化的;等邏輯都通了,下面會示範一個最小的防呆做法,把這個崩潰點補起來。
「輸入錯誤就崩潰」是這類作業很常見的一關,下面先看崩潰現場,再看最小的修法。
崩潰現場:已經輸入的商品全部消失
輸入順序:1→蘋果→30→abc。
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:請輸入新產品:請輸入 蘋果 價格:產品 蘋果 新增完成!
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:Traceback (most recent call last):
File "C:\demo\demo.py", line 5, in <module>
choice = int(input('請輸入功能選項:'))
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'abc'
這裡有一個比錯誤訊息更重要的觀察:這支程式的資料只存在變數 fruitPrices 裡,程式一崩潰,剛輸入的商品就全部消失。所以防呆不只是「好看」,而是在保護使用者辛苦輸入的資料。(Python 3.11 之後的錯誤訊息會多出 ~~~^^^ 標示錯誤位置,舊版沒有,版本不同箭頭符號會略有差異。)
修正:替換選項轉型的區塊,改成 try/except + continue
fruitPrices = {}
while True:
print('{:*^20}'.format('水果商品管理程式'))
print('(1).新增產品 (4).目前產品 (5).離開')
try:
choice = int(input('請輸入功能選項:'))
except ValueError:
print('請輸入數字 1~5')
continue
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()同樣的輸入順序(1→蘋果→30→abc→4→5),執行結果:
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:請輸入新產品:請輸入 蘋果 價格:產品 蘋果 新增完成!
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:請輸入數字 1~5
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:目前產品:
蘋果=30
******水果商品管理程式******
(1).新增產品 (4).目前產品 (5).離開
請輸入功能選項:離開程式
打了 abc 只被提示「請輸入數字 1~5」,回到選單後 4 依然能列出先前新增的蘋果。continue 是關鍵:它讓迴圈直接跳回下一輪,而不是往下走到 choice 尚未被賦值的分支。
資料沒了、程式也沒存:我的兩個習慣
說到「存檔」,還有一個很實際的習慣想提醒:程式本身就是文字檔的一種,輸入了很多內容卻忘了儲存,一當機就白費。所以我的習慣是寫完一個段落就執行一次程式;在編輯器有開啟「執行前自動儲存」(例如 VS Code 的相關設定)時,執行也會順便存檔,但不要完全依賴,仍然要適時按快捷鍵 Ctrl+S。
要特別分清楚:這是在保護「程式碼」。前面崩潰時消失的「商品資料」只存在變數裡,跑一次程式並不會讓它持久保存;要保留商品資料,得另外把資料寫進檔案或資料庫,這正是下面延伸路線的起點。
做完之後,學生接著想做什麼
這個作業做完,學生通常會沿著一條路線往下走:
1. 學到存檔,就會把這支管理程式改成有儲存和讀取功能,不再一關掉資料就沒了。 2. 學會資料庫之後,開始用資料庫的思維去設計資料表,資料更安全,查詢也更快。 3. 學會 tkinter 之後,就會希望把程式做成 GUI 介面,把資料庫、爬蟲結合起來,讓整個使用體驗更完整。
小結
整支程式其實就是一句話:dict 的 CRUD 操作,外面套一層 while True 選單。新增前用 in 擋重複、刪除和修改前用 not in 擋不存在,兩個方向剛好相反;del dict[key] 負責刪、直接賦值 dict[key] = 值 兼職新增和修改、.items() 負責走訪印出全部內容。這四個操作組合起來,幾乎就是任何一支「用字典存資料的小型管理程式」的骨架,換成學生成績、書籍庫存、會員資料,套路都一樣。
如果你還沒有練過 dict 的基本操作,可以先回頭複習變數、流程控制與資料型態,再回來對照這支程式會更清楚每一行在做什麼。