Python

Python 資料結構教學:List、Dict、Set、Tuple 一次搞懂

Python 的資料結構分成四種:list 有序、可重複、可修改;dict 用 key 對應 value;set 不重複、專門用來去重;tuple 有序但不可修改。AI 回傳的資料幾乎都是這四種的組合,看懂它們,你才能讀懂並精準修改 AI 產出的程式碼。
Python 資料結構教學:List、Dict、Set、Tuple 一次搞懂

Python 資料結構教學:List、Dict、Set、Tuple 一次搞懂

如果你曾經看著 AI 生成的程式碼裡一堆中括號和大括號發呆,這篇就是為你寫的。AI 回傳的資料幾乎都是結構化資料:API 回應通常是一個 dict,批次結果通常是一個 list。看不懂這些容器裝了什麼,你就只能照抄 AI 給的程式碼,卻抓不出它哪裡錯。

這篇會用範例帶你搞懂 Python 四種最常用的資料結構:list、dict、set、tuple,還有 AI 最愛寫也最容易寫錯的巢狀結構、推導式,以及兩個連資深工程師都會踩的陷阱。看完之後,你會多一個判斷 AI 給的程式碼對不對的能力。

你將學到什麼

四種容器一次分清楚

list、dict、set、tuple 誰有序、誰能重複、誰能修改,一個口訣記住。

list 與 dict 的核心操作

增刪查改怎麼寫,AI 最常漏掉的錯誤處理是哪一種。

set 去重與 tuple 固定值

集合運算怎麼用,函式回傳多個值為什麼會變成 tuple。

巢狀結構與推導式

看懂 list of dict,一行推導式的讀法怎麼拆解。

AI 最容易寫錯的兩個陷阱

淺複製污染原始資料、可變預設參數悄悄累積,怎麼抓出來。

三道遞進題自我檢測

讀懂、改寫、抓錯,練完就知道自己是不是真的學會了。

看到一個資料結構,先問自己四個問題它是有序的嗎?(list、tuple 有序;set、dict 無序)
可以重複嗎?(list、tuple 可以;set 不行;dict 的 key 不行)
可以修改嗎?(list、dict、set 可以;tuple 不行)
怎麼取值?(list、tuple 用索引;dict 用 key;set 不能直接取)
[ ]list有序、可重複、可修改{ }dictkey 對應 value{ }set不重複、無序()tuple有序、不可修改
四種容器一次看懂:list 用中括號、dict 與 set 用大括號、tuple 用小括號且不可修改。

串列 list:有序、可重複、可修改

list 是 Python 最常用的資料結構,用方括號 [] 包住,可以裝任何型別。AI 產出的程式九成以上都有 list,你要能讀懂它的增刪查改四個動作。

fruits = ["apple", "banana", "strawberry"]

# 取值(索引從 0 開始)
fruits[0]          # "apple"
fruits[-1]         # "strawberry"(最後一個)

# 新增
fruits.append("mango")     # 加到最後
fruits.insert(1, "grape")  # 插入指定位置

# 刪除
fruits.remove("banana")   # 刪除第一個符合的值
fruits.pop()              # 刪除並回傳最後一個
fruits.pop(0)              # 刪除並回傳第 0 個

# 查詢
"apple" in fruits       # True
fruits.index("strawberry")  # 2(回傳索引)
len(fruits)             # 元素數量
Vibe Coder 觀察重點看到 .append() 是加到最後; .insert(i, x) 是插入到第 i 個位置。 .remove(x) 找不到 x 會拋出 ValueError ,AI 常忘記加錯誤處理。

排序與切片:sort 跟 sorted 不一樣

nums = [3, 1, 4, 1, 5, 9]

nums.sort()              # 原地排序(改變自身):[1,1,3,4,5,9]
sorted(nums)             # 回傳新串列(不改自身)
nums.sort(reverse=True)  # 反向排序
nums.reverse()           # 反轉串列

# 切片(回傳新 list,不改原始)
nums[1:4]               # 取索引 1~3
nums[::2]                # 每隔一個取一個
nums[::-1]               # 反轉(同 reverse 但不修改原串列)

# 複製 list(重要!)
a = [1, 2, 3]
b = a          # b 和 a 指向同一個 list!
c = a.copy()   # c 是獨立的複本
c = a[:]       # 同上,切片複製
危險:直接用 = 複製 list b = a 不是複製,是讓 b 和 a 共用同一份資料,改 b 就會改到 a。
a = [1, 2, 3]
b = a          # b 和 a 共用同一個 list
b.append(4)
print(a)       # [1, 2, 3, 4](a 也被改了!)
Vibe Coder 觀察重點:sort 跟 sorted() 差在哪 .sort() 直接改原 list,沒有回傳值(回傳 None)。 sorted(x) 回傳新 list,原 list 不變。AI 有時候寫 result = my_list.sort() ,拿到的是 None 而不是排好的串列。

字典 dict:key-value 配對

dict 用 {} 包住,每個元素是「key:value」配對。API 回應幾乎都是 dict 或 dict 的 list,這是最常接觸的結構。

user = {"name": "Joan", "age": 28, "city": "Taipei"}

# 取值
user["name"]             # "Joan"(key 不存在會拋 KeyError)
user.get("email")        # None(key 不存在回傳 None,不報錯)
user.get("email", "N/A")  # "N/A"(指定預設值)

# 新增 / 修改
user["email"] = "j@example.com"  # 新增 key
user["age"] = 29                  # 修改現有 key

# 刪除
del user["city"]          # 刪除指定 key
user.pop("age")           # 刪除並回傳值

# 遍歷
for key in user:                    # 遍歷所有 key
    print(key, user[key])
for k, v in user.items():          # 同時拿 key 和 value
    print(f"{k}: {v}")
Vibe Coder 觀察重點:一律用 .get() 取值dict[key] 取值,key 不存在就 KeyError 崩潰。AI 常忘記這件事。看到 AI 寫 data["field"] ,問它:「這個 key 一定存在嗎?如果不存在,請改用 .get() 並指定預設值。」

dict 進階:合併、更新與推導式

d1 = {"a": 1, "b": 2}
d2 = {"b": 99, "c": 3}

# 合併(Python 3.9+)
merged = d1 | d2         # {"a":1, "b":99, "c":3}(d2 的 b 蓋掉 d1)

# 更新(就地)
d1.update(d2)            # d1 被更新,不回傳新 dict

# 取出所有 key / value / 配對
d1.keys()                # dict_keys(["a", "b"])
d1.values()              # dict_values([1, 2])
d1.items()               # dict_items([("a",1), ("b",2)])

# 判斷 key 存在
"a" in d1                # True(只查 key,不查 value)
1 in d1.values()          # True(查 value 要用 values 方法)
names = ["Alice", "Bob", "Carol"]

# 建立 {名字:名字長度} 的字典
name_len = {name: len(name) for name in names}
# {"Alice": 5, "Bob": 3, "Carol": 5}
常見誤解:in 只查 key "Joan" in d 查的是 key,不是 value,就算 Joan 是某個 value 也會回傳 False。
d = {"name": "Joan", "age": 28}
print("Joan" in d)   # False!in 查的是 key,不是 value
print("name" in d)   # True

集合 set 與元組 tuple

set:無序、不重複,用來去重跟做集合運算

tags = {"Python", "AI", "Python"}   # 自動去重
print(tags)                          # {"Python", "AI"}(順序不固定)

# 新增 / 刪除
tags.add("Vibe")
tags.discard("AI")      # 不存在也不報錯(remove 會報錯)

# 集合運算
a = {1, 2, 3}
b = {2, 3, 4}
a & b                    # 交集:{2, 3}
a | b                    # 聯集:{1, 2, 3, 4}
a - b                    # 差集:{1}(在 a 但不在 b)

# 最常用:去除 list 重複值
names = ["Alice", "Bob", "Alice"]
unique = list(set(names))   # ["Alice", "Bob"](順序不保證)

tuple:有序、不可修改,用來裝固定資料

point = (10, 20)          # 座標,不應被修改
x, y = point               # 解包(unpacking)

# 函式回傳多個值時,Python 自動打包成 tuple
def min_max(lst):
    return min(lst), max(lst)   # 回傳 tuple
lo, hi = min_max([3, 1, 4])     # 解包接收
四種結構一行總結 list :有序可改,用來存一批資料; dict :key-value,用來查找; set :不重複,用來去重和比較; tuple :不可改,用來裝固定值(如座標)。

巢狀結構與推導式

AI 回傳的 API 資料通常是「list 裡面裝 dict」的巢狀結構。推導式則讓你用一行取代四行迴圈,是 AI 最愛寫的簡潔語法。

巢狀結構:list of dict

students = [
    {"name": "Alice", "score": 92},
    {"name": "Bob",   "score": 78},
    {"name": "Carol", "score": 85},
]

# 取出第一個學生的名字
students[0]["name"]        # "Alice"

# 遍歷所有學生
for s in students:
    print(f"{s['name']}:{s['score']}")

串列推導式:一行取代 for 迴圈

nums = [1, 2, 3, 4, 5]

# 傳統寫法(五行)
squares = []
for n in nums:
    squares.append(n ** 2)

# 推導式(一行)
squares = [n ** 2 for n in nums]            # [1, 4, 9, 16, 25]
evens   = [n for n in nums if n % 2 == 0]   # [2, 4](加條件)

# 取出所有學生名字(從 list of dict)
names = [s["name"] for s in students]       # ["Alice","Bob","Carol"]
Vibe Coder 觀察重點看到 [... for ... in ...] 就是推導式,等同於一個 for 迴圈加 append。讀法:「對 xxx 裡的每個元素,產生 yyy」。加了 if 就是「只取滿足條件的」。

AI 最容易踩的兩個資料結構陷阱

陷阱一:直接用 = 複製 list 或 dict(淺複製問題)

a = b 不是複製,是共用同一份資料淺複製的問題不會馬上崩潰,而是悄悄污染原始資料。
original = [1, 2, 3]
copy = original      # 同一個 list!
copy.append(4)
print(original)      # [1, 2, 3, 4](原始也被改了)
用 .copy() 或 list() 建立真正的複本切片 [:] 也是同樣效果。
copy = original.copy()  # 或 copy = list(original)
copy = original[:]      # 切片複製也可以

陷阱二:函式預設參數用 list 或 dict(可變預設參數)

函式預設參數用 list 或 dict items=[] 只在函式定義時建立一次,之後每次呼叫都共用同一個 list。
def add_item(item, items=[]):   # [] 只建立一次!
    items.append(item)
    return items
print(add_item("a"))  # ["a"]
print(add_item("b"))  # ["a", "b"](不是 ["b"]!)
預設值用 None,函式內再建立這是 Python 最經典的隱性 bug 之一,值得背下來。
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

三道遞進題:讀懂、改寫、抓錯

為什麼要做這三道題
  • 讀懂:API 回傳巢狀結構時,你要能逐行追蹤資料流向
  • 改寫:AI 常用暴力 for 迴圈,你要能請它改用推導式或 dict
  • 抓錯:淺複製和 KeyError 是最隱性的 bug,必須看得出來

題目一 讀懂(基礎)

逐行說明以下程式碼在做什麼:說明 passedresult 分別是什麼結構、存了什麼值,並寫出 print 的輸出結果。

students = [
    {"name": "Alice", "score": 92},
    {"name": "Bob",   "score": 58},
    {"name": "Carol", "score": 75},
]
passed = [s["name"] for s in students if s["score"] >= 60]
result = {s["name"]: s["score"] for s in students}
print(passed)
print(result)
答案拆解
  1. passed 是串列推導式,從 students 取出 score 大於等於 60 的學生名字,結果是 ["Alice", "Carol"] (Bob 58 分被過濾)。
  2. result 是字典推導式,把所有學生的名字當 key、分數當 value,結果是 {"Alice": 92, "Bob": 58, "Carol": 75}
  3. 輸出: ["Alice", "Carol"]{"Alice": 92, "Bob": 58, "Carol": 75}

題目二 改寫(進階)

真實資料常有重複,AI 給的初版程式常用暴力 for 迴圈去重。懂得用 set 和 dict 去重,才能讓程式更快、更可靠,也能精準告訴 AI 該用哪種方式改寫。

# 原版:用 for 迴圈去重,效能較差
tags = ["Python", "AI", "Python", "Data", "AI"]
unique = []
for tag in tags:
    if tag not in unique:
        unique.append(tag)

任務:用以下提示詞請 AI 改寫,並說明改寫後的優缺點。

「請用三種方式改寫這段去重程式:
1. 用 set 去重(最快,但不保留順序)
2. 用 dict.fromkeys() 去重(保留順序)
3. 用 list 推導式加 seen set 去重(保留順序,最易讀)
每種方式加中文說明優缺點。」
答案拆解
  1. list(set(tags)) 最快,但 set 無序,去重後順序不保證。
  2. list(dict.fromkeys(tags)) 一樣快,且保留第一次出現的順序,是 Python 3.7 之後保留順序去重最簡潔的寫法。
  3. 推導式版:搭配一個 seen 集合邊走訪邊記錄,保留順序且可讀,適合需要搭配其他條件的情況。

題目三 抓錯(高階)

資料結構的錯誤很少直接崩潰,更常是悄悄產出錯誤結果:複製問題污染原始資料、KeyError 在邊界才爆出,是最難 debug 的一種。

def process_students(students, results={}):
    for s in students:
        name = s["name"]
        score = s["score"]
        results[name] = score
    return results

batch1 = [{"name": "Alice", "score": 92}]
batch2 = [{"name": "Bob",   "score": 78}]

r1 = process_students(batch1)
r2 = process_students(batch2)
print(r2)   # 你預期是什麼?

任務:找出兩個問題,說明 print(r2) 的實際輸出,並寫出修正提示詞。

問題一:可變預設參數 results={} results={} 只在函式定義時建立一次,之後每次呼叫共用同一個 dict。 r1 後 results 已有 Alice, r2 後再加 Bob,所以 print(r2) 實際是 {"Alice": 92, "Bob": 78} ,不是預期的 {"Bob": 78}
問題二:直接用 s["score"] 取值,沒有防護如果傳入的 dict 缺少 "score" key,程式會拋出 KeyError 崩潰,應改用 s.get("score", 0) 提供預設值。
給 AI 的修正提示詞「這段函式有兩個問題:一、預設參數 results={} 改成 results=None ,函式內判斷 if results is None: results = {} ;二、 s["score"] 改用 s.get("score", 0) 防 KeyError。修正後加中文註解。」

術語速查表

術語說明
list 有序、可重複、可修改的序列,用 [] 建立。
.append(x) 將 x 加到 list 末尾; .insert(i, x) 插入到第 i 個位置。
.sort() 對比 sorted() .sort() 就地排序回傳 None; sorted() 回傳新 list。
.copy() 建立 list 的淺複本;直接用 = 是共用同一份資料。
dict key-value 配對,key 不可重複,用 {}dict() 建立。
.get(key, default) 安全取 dict 值;key 不存在時回傳 default,不拋 KeyError。
.items() 回傳所有 key-value 配對,常搭配 for k, v in d.items()
set 無序、不重複,用 {值} 建立(空 set 要用 set() ,不能用 {} )。
.discard(x) 從 set 刪除 x;x 不存在時不報錯( .remove(x) 會報錯)。
tuple 有序、不可修改的序列,用 () 建立;常用於函式回傳多個值。
串列推導式 [運算式 for 變數 in 序列 if 條件] ,一行取代 for 加 append。
字典推導式 {key: value for 變數 in 序列} ,一行建字典。
dict.fromkeys() 快速建字典或去重(保留順序): list(dict.fromkeys(lst))
淺複製只複製最外層;巢狀結構內部仍共用。 .copy()[:] 都是淺複製。
可變預設參數陷阱函式預設參數不能用 list 或 dict,因為只建立一次,多次呼叫共用。

重點整理與檢查清單

需求用哪個理由
有序、可重複、可修改 list 最通用的序列
用名字查找值 dict 查找速度快
去重、集合運算 set 自動去重、成員檢查快
固定不可改的資料 tuple 防止意外修改
API 回應、資料庫結果 list of dict 最常見的巢狀格式
Vibe Coder 四問口訣有序?可重複?可修改?怎麼取值?問完這四個,你就知道 AI 選的結構對不對。

模組三完成清單

  • 能說明 list、dict、set、tuple 的差異和各自用途
  • 知道 .sort()sorted() 的差別
  • 知道 = 複製 list 是共用,要用 .copy()
  • 能用 .get() 安全取 dict 值,避免 KeyError
  • 看懂串列推導式和字典推導式
  • 完成作業題目一:說明推導式的結構和輸出
  • 完成作業題目二:用三種方式去重並說明優缺點
  • 完成作業題目三:找出可變預設參數和 KeyError 問題

延伸學習

把這篇文章分享給需要的人FacebookLINEThreadsX

常見問答

list 和 tuple 有什麼差別?
兩者都是有序、可重複的序列,差別在 tuple 建立後不能修改,list 可以。固定不變的資料(例如座標)適合用 tuple,會持續增刪的資料用 list。
為什麼 dict 要用 .get() 取值,不能直接用中括號嗎?
可以,但 key 不存在時中括號寫法會直接拋出 KeyError 讓程式崩潰;.get() 在 key 不存在時回傳 None 或你指定的預設值,比較安全,尤其是處理 AI 或 API 回傳、不保證每個 key 都存在的資料時。
b = a 為什麼不是複製 list?
= 只是讓 b 和 a 指向同一份資料,修改 b 也會改到 a。要建立真正獨立的複本,要用 a.copy()、list(a) 或切片 a[:]。
函式的預設參數為什麼不能寫 items=[]?
list 或 dict 當預設參數只會在函式定義時建立一次,之後每次呼叫都共用同一份,資料會悄悄累積。正確做法是預設值寫 None,在函式內部再判斷建立新的 list 或 dict。
set 和 dict 去重有什麼差別?
list(set(x)) 最快但不保留順序;list(dict.fromkeys(x)) 一樣快,還保留第一次出現的順序,是保留順序去重最簡潔的寫法。