🗺️ 程式互動式地圖
零基礎起步・第 6 課

字典:用名稱查資料

串列要靠位置編號找資料,可是你記得的往往是名字,不是「第幾個」。這一課用字典把名稱和資料配成一對,直接用名稱查,也看懂最常見的 KeyError。

逐行執行+記憶體動畫 KeyError 與 get() 字典操作台

💡 先搞懂問題

上一課學了串列(list,一排有編號的格子)。假設你想用它記同事的分機號碼(公司內線電話,四碼),大概會寫兩個串列:names = ["阿明", "小華"]、phones = ["1201", "1305"],約定好「同一個位置的名字和電話是一對」。要查小華的電話,得先找出小華在 names 的第幾格,再拿那個索引去 phones 取值;哪天刪了一個名字卻忘了刪對應的電話,後面每一對就全部錯位,而且程式不會報錯。

問題出在:你心裡想的是「用名字查電話」,串列卻只能「用位置查資料」。Python 的字典(dictionary,簡稱 dict)就是為這件事設計的:把「名稱」和「資料」直接配成一對,寫成 phone = {"阿明": "1201", "小華": "1305"}。冒號左邊用來查的叫鍵(key),右邊查到的叫值(value),一組 鍵: 值 叫做一個鍵值對(key-value pair)。查小華的電話只要寫 phone["小華"],方括號裡放的是鍵,不是位置編號。

兩個串列:靠「同一個位置」配對 字典:名稱直接配資料 namesphones '阿明'0 '小華'1 '1201'0 '1305'1 查小華:先找她在 names 的第幾格(1), 再拿 1 去 phones 取值。 只刪了一邊的資料,後面全部錯位 phone 字典 dict · o1 鍵 key值 value '阿明' '1201' '小華' '1305' phone["小華"] 用鍵直接查到值,不必知道它在第幾個。 一個鍵配一個值,不會錯位。
圖 1 兩個串列靠位置配對,字典靠鍵配對。右邊的畫法和播放器相同:名字(phone)貼在一個字典物件上,物件裡左欄是鍵、右欄是值。

想想手機裡的通訊錄,或是公司的分機表。你要打給小華,是在搜尋框打「小華」,螢幕就跳出她的號碼;你從來不需要知道她是通訊錄裡的第幾個人。要新增聯絡人,就輸入名字和號碼存起來;小華換了號碼,就打開「小華」把號碼改掉,而不是再多存一個小華。字典就是程式裡的通訊錄:聯絡人姓名是鍵,電話號碼是值。

公司通訊錄 🔍 小華 小華 1305 阿明 1201 美玲 1422 回到程式 phone["小華"] # → '1305' 姓名 = 鍵 key 號碼 = 值 value 搜尋姓名就出現號碼;不用知道是第幾個人。 同一個姓名只會有一筆,改號碼是覆蓋舊的。
圖 2 通訊錄的比喻:搜尋框裡打的姓名就是鍵,查到的號碼就是值。

字典最常讓新手卡住的地方,是「查不到」的時候。在串列裡,索引超出範圍會得到 IndexError;在字典裡,用一個不存在的鍵去查,會得到 KeyError,程式在那一行就停下來。如果「查不到」是正常情況(例如新顧客還沒有點數),就改用 get() 方法:查不到時它不報錯,而是交回 None,或交回你指定的預設值。

字典裡沒有「阿傑」時,三種查法的結果 phone["阿傑"] phone.get("阿傑") phone.get("阿傑", "查無此人") KeyError: '阿傑' 程式在這一行停下來 None 不報錯,交回「沒有值」 '查無此人' 不報錯,交回你指定的預設值
圖 3 方括號查不到就出錯;get() 查不到時交回 None 或預設值。兩種都對,看「查不到」在你的程式裡算不算意外。
回到程式:剛才的通訊錄對應的是字典物件,聯絡人姓名是鍵,電話號碼是值,搜尋姓名就是 phone["小華"],新增或修改聯絡人都是 phone["名字"] = "號碼"。比喻有三個地方和真正的字典不同。第一,手機通訊錄允許兩個同名的聯絡人,字典不行:同一個鍵只能有一個值,再存一次就是覆蓋。第二,手機可以用號碼反查是誰,字典的 in 和方括號都只認鍵,不會去值裡面找。第三,通訊錄多半照姓名排序,字典則是照你放進去的先後順序排列(Python 3.7 起保證)。

🎮 互動實驗室一:查、新增、修改、刪除

按「下一步 ▶」讓 Python 執行一行,看右邊的記憶體怎麼變。字典物件畫成兩欄:左欄綠底是鍵,右欄是值。遇到 🤔 問題先選答案,再按下一步核對。右上角的選單可以切換到第二段程式:用 for 迴圈把字典裡的資料一筆一筆拿出來。

畫面說明:在「通訊錄」這段裡,請比較第 3 行和第 4 行:同樣是 phone[鍵] = 值,鍵不存在時會在最下面多出一列(新增),鍵已經存在時只換掉那一列的值、位置不動(修改)。第 7 行拿號碼去問 in,結果是 False,因為 in 只看左欄。最後一行用不存在的鍵查,輸出畫面出現紅色的 KeyError。切到「走訪」那段,注意 for 迴圈每一圈都把名字 name 改貼到下一個鍵,順序就是當初放進字典的順序。

🎮 互動實驗室二:計數,和一筆一筆的記錄

字典最常見的用途之一是計數:統計每個詞出現幾次、每種商品賣了幾杯。下面的程式數一串動物名稱各出現幾次,兩種寫法只差第 4 行。先用預設的 get() 寫法走完一遍,再切到 counts[w] + 1 寫法,看看它在第幾步出錯、為什麼。for 迴圈這裡只要知道「把串列裡的元素一個一個交給 w」,細節第 8 課會講。

畫面說明:用 get() 的版本,第一次遇到「貓」時字典裡還沒有這個鍵,counts.get(w, 0) 交回預設值 0,加 1 之後存成 1;第二次遇到「貓」,get 交回的是 1,存成 2。整個過程只有一個字典物件,每圈都在閃黃,代表就地修改。換成 counts[w] + 1 的版本,第一圈就要先讀 counts["貓"],可是字典是空的,所以在第 4 行就丟出 KeyError:等號右邊要先算完,才輪得到「新增」。

串列裡放字典:一筆一筆的記錄

一個字典可以描述一筆資料的各個欄位(姓名、分數),把很多筆字典放進串列,就成了一張表格:串列的每一格是一列,字典的鍵是欄名。下面的程式建立三位學員的成績,算出總分,再透過另一個名字 best 修改其中一筆。注意看第 9 行:best = rows[1] 會不會複製出一個新字典?這和上一課串列的共用是同一件事。

畫面說明:右邊的串列 rows 有三格,每一格裡的小圓點用箭頭指向一個字典。for 迴圈每一圈讓 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/FalseFalse
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 不加任何方法時,拿到的是鍵。

原本鍵不存在 → 新增在最後鍵已存在 → 只換值 phone["美玲"] = "1422"phone["阿明"] = "1288" '阿明''1201' '小華''1305' '阿明''1201' '小華''1305' '美玲''1422' '阿明''1288' '小華''1305' 多一列,len 從 2 變 3 還是 2 列,阿明仍在第一列 同一種寫法 d[鍵] = 值,Python 先看鍵在不在:在就覆蓋,不在就新增。鍵不會重複。
圖 4 新增與修改用的是同一種寫法。修改不會改變鍵的位置;新增的鍵值對一律排在最後。

3. in 只看鍵

"小華" in phone 問的是「有沒有小華這個鍵」。拿號碼去問 "1305" in phone 會得到 False,即使那個號碼明明就在字典裡,因為 in 不會去值裡找。真的要查值,要寫 "1305" in phone.values(),但這需要一筆一筆比對,失去了字典「用鍵直接找到」的好處。如果你常常需要用號碼反查姓名,比較好的做法是另外建一個「號碼 → 姓名」的字典。

鍵(in 只看這一欄)值(in 不看) '阿明''1201' '小華''1305' '美玲''1422' "小華" in phone → True "1305" in phone → False 號碼在值那一欄,in 根本沒去找
圖 5 in 只檢查鍵。方括號、get、del、pop 也都一樣,全部是「用鍵」做事。

4. 什麼可以當鍵:不可變的值

字典能「用鍵直接找到」資料,是因為它會根據鍵的內容算出一個號碼(叫做雜湊值,hash),用這個號碼決定資料放在哪裡;查詢時再算一次,就知道去哪裡拿。這個方法有一個前提:鍵的內容不能改變,否則改了之後算出來的號碼不一樣,就找不到原本那一筆了。所以 Python 只接受不可變的值當鍵:字串、整數、浮點數、布林值、元組都可以;串列、字典這類可變物件不行,會得到 TypeError: unhashable type: 'list'(unhashable 意思是「無法算雜湊值」)。元組要當鍵,裡面也不能含串列。

✔ 可以當鍵(不可變) ✘ 不能當鍵(可變) "小華"字串 2026整數 (3, 5)元組(座位) ["貓", "狗"]串列 {"a": 1}字典 (1, [2])含串列的元組 內容固定,算出的號碼永遠一樣 TypeError: unhashable type: 'list' 內容可能被改,號碼就對不上
圖 6 鍵必須是不可變的值。值沒有任何限制,串列、字典都可以放在值的位置。

5. 字典會保持放入的順序

從 Python 3.7 起,語言規格保證字典會照鍵值對放進去的先後順序排列:印出來、用 for 走訪、keys() 都是這個順序。修改既有鍵的值不會改變它的位置;刪掉之後再加回來,會排到最後。網路上較舊的文章可能寫「字典沒有順序」,那是 3.6 以前的情況。字典沒有「第幾個」的索引,phone[0] 會被當成「找鍵 0」,得到 KeyError: 0。

6. 串列裡放字典:表格資料的雛形

一個字典描述一筆記錄,很多筆記錄放進串列,就是一張表:串列決定「第幾列」,字典的鍵決定「哪一欄」。讀取時先用索引挑列、再用鍵挑欄,例如 rows[1]["score"]。以後學資料分析時,pandas 的 DataFrame 可以直接把這種「字典組成的串列」轉成表格;從 API 拿到的 JSON 資料,在 Python 裡也幾乎都是這個形狀。

rows list 0 1 2 'name''阿明''score'82 'name''小華''score'95 'name''美玲''score'77 同樣的資料看成表格 列namescore 0阿明82 1小華95 2美玲77 rows[1]["score"] → 95 先用索引挑列,再用鍵挑欄 pandas 的 DataFrame 就是這個概念
圖 7 串列裡放字典:每一格箭頭指向一筆記錄。左邊是記憶體裡的樣子,右邊是我們習慣看的表格。

7. 計數模式

counts[w] = counts.get(w, 0) + 1 這一行值得背起來,它的意思是「w 的次數,等於原本的次數(還沒出現過就當作 0)再加 1」。為什麼不能寫 counts[w] = counts[w] + 1?因為指派會先算等號右邊,第一次遇到 w 時字典裡還沒有這個鍵,讀 counts[w] 就 KeyError 了。同樣的模式也能拿來累加金額、杯數,把 1 換成要加的數量就好。以後學到標準函式庫,collections.Counter 可以一行做完計數,但它背後的想法就是這一行。

counts[w] = counts.get(w, 0) + 1 第 1 次「貓」counts 裡沒有 get → 0(預設值) 0 + 1 {'貓': 1} 第 2 次「貓」已經有了 get → 1 1 + 1 {'貓': 2} counts[w] = counts[w] + 1 第 1 次遇到「貓」時,右邊要先讀 counts["貓"],鍵還不存在 → KeyError: '貓'
圖 8 計數模式的兩圈。關鍵是 get 的預設值 0:讓「第一次出現」和「之後再出現」可以用同一行程式處理。

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)
"a" 已經存在,d["a"] = 5 是修改:值換成 5,位置仍在第一個。"c" 不存在,所以新增在最後。字典的鍵不會重複,D 不可能出現。

Q2.下列程式印出什麼?

prices = {"蘋果": 30}
print(prices.get("香蕉", 0) + prices["蘋果"])
"香蕉" 不存在,但用的是 get 並給了預設值 0,所以不會出錯,得到 0;prices["蘋果"] 是 30,相加得 30。會出 KeyError 的是 prices["香蕉"] 這種方括號寫法。

Q3.下列程式印出什麼?

d = {"x": 10}
print(10 in d, "x" in d)
in 只檢查鍵。字典只有一個鍵 "x",10 是值,所以 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))
元組 (1, 2) 是不可變的,可以當鍵;第 3 行的串列放在「值」的位置,沒有限制;第 4 行拿串列當鍵,Python 無法替它算雜湊值,丟出 TypeError,print 不會執行。指派新的鍵不會 KeyError,只有讀取不存在的鍵才會。

Q5.下列程式印出什麼?

counts = {}
for w in ["紅", "藍", "紅"]:
    counts[w] = counts.get(w, 0) + 1
print(counts)
第一圈 "紅" 不存在,get 交回 0,存成 1;第二圈 "藍" 存成 1;第三圈 "紅" 已有 1,存成 2。修改不改變位置,"紅" 仍排第一,所以不是 C。用了 get,不會 KeyError。

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 當成了一份影本。