💡 先搞懂問題
上一課學了串列(list,一排有編號的格子)。假設你想用它記同事的分機號碼(公司內線電話,四碼),大概會寫兩個串列:names = ["阿明", "小華"]、phones = ["1201", "1305"],約定好「同一個位置的名字和電話是一對」。要查小華的電話,得先找出小華在 names 的第幾格,再拿那個索引去 phones 取值;哪天刪了一個名字卻忘了刪對應的電話,後面每一對就全部錯位,而且程式不會報錯。
問題出在:你心裡想的是「用名字查電話」,串列卻只能「用位置查資料」。Python 的字典(dictionary,簡稱 dict)就是為這件事設計的:把「名稱」和「資料」直接配成一對,寫成 phone = {"阿明": "1201", "小華": "1305"}。冒號左邊用來查的叫鍵(key),右邊查到的叫值(value),一組 鍵: 值 叫做一個鍵值對(key-value pair)。查小華的電話只要寫 phone["小華"],方括號裡放的是鍵,不是位置編號。
想想手機裡的通訊錄,或是公司的分機表。你要打給小華,是在搜尋框打「小華」,螢幕就跳出她的號碼;你從來不需要知道她是通訊錄裡的第幾個人。要新增聯絡人,就輸入名字和號碼存起來;小華換了號碼,就打開「小華」把號碼改掉,而不是再多存一個小華。字典就是程式裡的通訊錄:聯絡人姓名是鍵,電話號碼是值。
字典最常讓新手卡住的地方,是「查不到」的時候。在串列裡,索引超出範圍會得到 IndexError;在字典裡,用一個不存在的鍵去查,會得到 KeyError,程式在那一行就停下來。如果「查不到」是正常情況(例如新顧客還沒有點數),就改用 get() 方法:查不到時它不報錯,而是交回 None,或交回你指定的預設值。
phone["小華"],新增或修改聯絡人都是 phone["名字"] = "號碼"。比喻有三個地方和真正的字典不同。第一,手機通訊錄允許兩個同名的聯絡人,字典不行:同一個鍵只能有一個值,再存一次就是覆蓋。第二,手機可以用號碼反查是誰,字典的 in 和方括號都只認鍵,不會去值裡面找。第三,通訊錄多半照姓名排序,字典則是照你放進去的先後順序排列(Python 3.7 起保證)。🎮 互動實驗室一:查、新增、修改、刪除
按「下一步 ▶」讓 Python 執行一行,看右邊的記憶體怎麼變。字典物件畫成兩欄:左欄綠底是鍵,右欄是值。遇到 🤔 問題先選答案,再按下一步核對。右上角的選單可以切換到第二段程式:用 for 迴圈把字典裡的資料一筆一筆拿出來。
phone[鍵] = 值,鍵不存在時會在最下面多出一列(新增),鍵已經存在時只換掉那一列的值、位置不動(修改)。第 7 行拿號碼去問 in,結果是 False,因為 in 只看左欄。最後一行用不存在的鍵查,輸出畫面出現紅色的 KeyError。切到「走訪」那段,注意 for 迴圈每一圈都把名字 name 改貼到下一個鍵,順序就是當初放進字典的順序。🎮 互動實驗室二:計數,和一筆一筆的記錄
字典最常見的用途之一是計數:統計每個詞出現幾次、每種商品賣了幾杯。下面的程式數一串動物名稱各出現幾次,兩種寫法只差第 4 行。先用預設的 get() 寫法走完一遍,再切到 counts[w] + 1 寫法,看看它在第幾步出錯、為什麼。for 迴圈這裡只要知道「把串列裡的元素一個一個交給 w」,細節第 8 課會講。
counts.get(w, 0) 交回預設值 0,加 1 之後存成 1;第二次遇到「貓」,get 交回的是 1,存成 2。整個過程只有一個字典物件,每圈都在閃黃,代表就地修改。換成 counts[w] + 1 的版本,第一圈就要先讀 counts["貓"],可是字典是空的,所以在第 4 行就丟出 KeyError:等號右邊要先算完,才輪得到「新增」。串列裡放字典:一筆一筆的記錄
一個字典可以描述一筆資料的各個欄位(姓名、分數),把很多筆字典放進串列,就成了一張表格:串列的每一格是一列,字典的鍵是欄名。下面的程式建立三位學員的成績,算出總分,再透過另一個名字 best 修改其中一筆。注意看第 9 行:best = rows[1] 會不會複製出一個新字典?這和上一課串列的共用是同一件事。
r 改貼到下一個字典,r["score"] 讀的就是那一筆的分數。第 9 行之後,best 的箭頭和 rows 第 1 格的箭頭指向同一個字典,所以第 10 行透過 best 改成 100,印 rows[1] 也是 100。🎮 互動實驗室三:字典操作台
這是晴空咖啡的庫存字典 stock,鍵是飲料名稱、值是剩下的杯數。左邊每個按鈕是一行 Python,按下去就會「執行」那一行:右邊的表格顯示字典目前的內容,黑色區域像終端機一樣列出你執行過的程式和輸出。同一個按鈕在不同時機按,結果可能不同(例如先刪掉美式再刪一次),試著先預測再按。所有結果都和 Python 3.13 實際執行一致。
目前的 stock
📘 原理補完
1. 正式名稱與寫法
Python 官方文件把字典歸類為對應型別(mapping type):它把一組鍵對應到各自的值。字典用大括號建立,鍵值對之間用逗號分隔,{} 是空字典。字典和串列一樣是可變(mutable)物件,可以就地新增、修改、刪除鍵值對;所以兩個名字貼在同一個字典上時,透過任一個名字修改,另一個都看得到,這和上一課串列的共用完全相同。要另一份就用 d.copy()(同樣是只複製外層的淺複製)。
值可以是任何物件:數字、字串、串列,甚至另一個字典。鍵則有限制,後面第 4 點會說明。鍵不能重複,同一個鍵再指派一次就是覆蓋舊值。
2. 常用操作一覽
下表的 d 是一個字典,k 是鍵,v 是值。和上一課一樣,看懂一個操作要問:會不會改到原字典、交回什麼、什麼時候出錯。
| 寫法 | 做什麼 | 改到原字典? | 交回來的值 | 鍵不存在時 |
|---|---|---|---|---|
d[k] | 用鍵查值 | 不會 | 那個值 | KeyError |
d[k] = v | 鍵存在就修改,不存在就新增(排在最後) | 會 | (指派,不交回值) | 新增,不會出錯 |
d.get(k) | 用鍵查值 | 不會 | 那個值 | 交回 None |
d.get(k, 預設) | 用鍵查值 | 不會 | 那個值 | 交回預設值 |
k in d | 有沒有這個鍵 | 不會 | True/False | False |
del d[k] | 刪掉這一對 | 會 | (陳述式,不交回值) | KeyError |
d.pop(k) | 拿走這一對 | 會 | 被拿走的值 | KeyError |
d.pop(k, 預設) | 拿走這一對 | 會(鍵存在時) | 被拿走的值 | 交回預設值 |
len(d) | 有幾對鍵值 | 不會 | 整數 | — |
d.keys()/d.values()/d.items() | 所有鍵/所有值/所有 (鍵, 值) | 不會 | 檢視物件,可用 for 走訪或 list() 轉成串列 | — |
直接 print(stock.keys()) 會看到 dict_keys(['拿鐵', '美式', '紅茶']),這是一種「檢視」(view)物件,會跟著字典的內容變化;需要真正的串列時,包一層 list() 就好。for 名字 in d 不加任何方法時,拿到的是鍵。
3. in 只看鍵
"小華" in phone 問的是「有沒有小華這個鍵」。拿號碼去問 "1305" in phone 會得到 False,即使那個號碼明明就在字典裡,因為 in 不會去值裡找。真的要查值,要寫 "1305" in phone.values(),但這需要一筆一筆比對,失去了字典「用鍵直接找到」的好處。如果你常常需要用號碼反查姓名,比較好的做法是另外建一個「號碼 → 姓名」的字典。
4. 什麼可以當鍵:不可變的值
字典能「用鍵直接找到」資料,是因為它會根據鍵的內容算出一個號碼(叫做雜湊值,hash),用這個號碼決定資料放在哪裡;查詢時再算一次,就知道去哪裡拿。這個方法有一個前提:鍵的內容不能改變,否則改了之後算出來的號碼不一樣,就找不到原本那一筆了。所以 Python 只接受不可變的值當鍵:字串、整數、浮點數、布林值、元組都可以;串列、字典這類可變物件不行,會得到 TypeError: unhashable type: 'list'(unhashable 意思是「無法算雜湊值」)。元組要當鍵,裡面也不能含串列。
5. 字典會保持放入的順序
從 Python 3.7 起,語言規格保證字典會照鍵值對放進去的先後順序排列:印出來、用 for 走訪、keys() 都是這個順序。修改既有鍵的值不會改變它的位置;刪掉之後再加回來,會排到最後。網路上較舊的文章可能寫「字典沒有順序」,那是 3.6 以前的情況。字典沒有「第幾個」的索引,phone[0] 會被當成「找鍵 0」,得到 KeyError: 0。
6. 串列裡放字典:表格資料的雛形
一個字典描述一筆記錄,很多筆記錄放進串列,就是一張表:串列決定「第幾列」,字典的鍵決定「哪一欄」。讀取時先用索引挑列、再用鍵挑欄,例如 rows[1]["score"]。以後學資料分析時,pandas 的 DataFrame 可以直接把這種「字典組成的串列」轉成表格;從 API 拿到的 JSON 資料,在 Python 裡也幾乎都是這個形狀。
7. 計數模式
counts[w] = counts.get(w, 0) + 1 這一行值得背起來,它的意思是「w 的次數,等於原本的次數(還沒出現過就當作 0)再加 1」。為什麼不能寫 counts[w] = counts[w] + 1?因為指派會先算等號右邊,第一次遇到 w 時字典裡還沒有這個鍵,讀 counts[w] 就 KeyError 了。同樣的模式也能拿來累加金額、杯數,把 1 換成要加的數量就好。以後學到標準函式庫,collections.Counter 可以一行做完計數,但它背後的想法就是這一行。
8. 常見錯誤長什麼樣子
以下是 Python 3.13 實際執行的錯誤畫面(在你的電腦上,File 後面會是完整的檔案路徑)。KeyError 的最後一行只會寫出找不到的那個鍵,記得看它是字串還是數字、有沒有多空白或打錯字。
用不存在的鍵查值:
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(phone["阿傑"])
~~~~~^^^^^^^^
KeyError: '阿傑'
把字典當串列,用位置去查:字典沒有索引,0 被當成鍵。
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(phone[0])
~~~~~^^^
KeyError: 0
刪除不存在的鍵:del 與 pop(k) 都一樣;pop(k, 預設) 則不會出錯。
Traceback (most recent call last):
File "main.py", line 2, in <module>
del stock["美式"]
~~~~~^^^^^^^^
KeyError: '美式'
用串列當鍵:
Traceback (most recent call last):
File "main.py", line 2, in <module>
seen[["貓", "狗"]] = 1
~~~~^^^^^^^^^^^^^^
TypeError: unhashable type: 'list'
9. 兩段完整範例
第一段是會員點數,把查、新增、修改、刪除、走訪串起來;註解裡的輸出都是實際執行的結果。
points = {"A001": 120, "A002": 45} # 會員編號(鍵)→ 點數(值)
points["A003"] = 0 # 鍵不存在:新增一對,排在最後
points["A001"] = points["A001"] + 30 # 鍵已存在:先讀出舊值,加 30 再存回去
print(points) # {'A001': 150, 'A002': 45, 'A003': 0}
print(points.get("A009", 0)) # 0:查不到時交回預設值,不會出錯
print("A002" in points, 45 in points) # True False:in 只檢查鍵
removed = points.pop("A002") # 拿走 A002 這一對,並交回它的值
print(removed, len(points)) # 45 2
for member, pt in points.items(): # items():每一圈同時拿到鍵和值
print(member, pt) # A001 150,下一圈 A003 0
第二段是一筆筆訂單(串列裡放字典),用計數模式加總每個品項的杯數,最後示範元組當鍵。
orders = [ # 串列裡放字典:每一個字典是一筆訂單
{"item": "拿鐵", "qty": 2},
{"item": "紅茶", "qty": 1},
{"item": "拿鐵", "qty": 3},
]
total = {} # 空字典,準備依品項累加杯數
for o in orders: # o 每一圈貼到下一筆訂單
name = o["item"] # 用鍵取出這一筆的欄位
total[name] = total.get(name, 0) + o["qty"] # 第一次出現時從 0 開始加
print(total) # {'拿鐵': 5, '紅茶': 1}
print(list(total.keys()), list(total.values())) # ['拿鐵', '紅茶'] [5, 1]
seat = {(3, 5): "阿明"} # 元組可以當鍵:第 3 排第 5 位
print(seat[(3, 5)]) # 阿明
10. 以後會在哪裡遇到
讀 AI 應用的範例程式時,字典幾乎無所不在:呼叫大型語言模型 API 時,每一則訊息都寫成 {"role": "user", "content": "..."},整段對話就是「串列裡放字典」;模型的設定參數、API 回傳的 JSON 結果、pandas 用來建立表格的資料,也都是字典。看到大括號和冒號,就先找出「鍵是什麼、值是什麼」。
✅ 自我檢測
6 題,每題的答案都用 Python 3.13 實際執行確認過。選完立刻顯示對錯與解析,全部作答後會出現總分。目前得分:0 / 6
Q1.下列程式印出什麼?
d = {"a": 1, "b": 2}
d["a"] = 5
d["c"] = 3
print(d)
d["a"] = 5 是修改:值換成 5,位置仍在第一個。"c" 不存在,所以新增在最後。字典的鍵不會重複,D 不可能出現。Q2.下列程式印出什麼?
prices = {"蘋果": 30}
print(prices.get("香蕉", 0) + prices["蘋果"])
prices["蘋果"] 是 30,相加得 30。會出 KeyError 的是 prices["香蕉"] 這種方括號寫法。Q3.下列程式印出什麼?
d = {"x": 10}
print(10 in d, "x" in d)
10 in d 是 False,"x" in d 是 True。Q4.執行下列程式,結果是什麼?
info = {}
info[(1, 2)] = "A"
info["k"] = [1, 2]
info[[3, 4]] = "B"
print(len(info))
Q5.下列程式印出什麼?
counts = {}
for w in ["紅", "藍", "紅"]:
counts[w] = counts.get(w, 0) + 1
print(counts)
Q6.下列程式印出什麼?
rows = [{"n": "A", "s": 3}, {"n": "B", "s": 5}]
r = rows[0]
r["s"] = 9
print(rows[0]["s"] + rows[1]["s"])
r = rows[0] 沒有複製,r 和串列第 0 格指向同一個字典。透過 r 改成 9,rows[0]["s"] 也是 9,加上 rows[1]["s"] 的 5,得 14。選 A 的人把 r 當成了一份影本。