Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

容器:list/dict/set

Python 容器全攻略:list/dict/set 怎麼選

從一份社團報名名單出發,動手摸熟串列、元組、字典、集合四種容器,並理解它們底層的速度差異與選用時機。

從一份社團報名名單開始

假設你是某個社團的幹部,手上有一份報名名單,今天要做三件事:把名單照報名順序印出來、查某位同學的聯絡方式、還要算出「同時報名 A 活動跟 B 活動」的人有哪些。這三件事,剛好對應到 Python 裡三種最常用的容器(container):串列(list)、字典(dict)、集合(set)。

很多初學者寫程式時,不管什麼資料都塞進 list,結果查一筆資料要從頭找到尾,去重複還得自己寫一堆 if。其實 Python 早就準備好趁手的工具,重點是「選對容器」。這篇文章我們就一邊動手寫,一邊把 list、tuple、dict、set 四種容器摸熟,最後再聊聊它們底層到底快在哪裡。

打開你的 Python 直譯器(終端機輸入 python3)或任何編輯器,跟著一起敲,學起來最快。

容器:list/dict/set概念示意圖

串列(list):有順序、可變動的萬用容器

串列是最常用的容器,用中括號 [] 建立,裡面的元素有順序可以重複、而且可以隨時修改

members = ["小芸", "阿哲", "怡君"]
print(members)        # 輸出:['小芸', '阿哲', '怡君']
print(len(members))   # 輸出:3
print(members[0])     # 輸出:小芸(索引從 0 開始)
print(members[-1])    # 輸出:怡君(負索引從尾巴數)

增、刪、改、查

新增元素用 append()(加在最後)或 insert()(插在指定位置):

members.append("大文")          # 加在最後
members.insert(1, "佳玲")       # 插在索引 1 的位置
print(members)
# 輸出:['小芸', '佳玲', '阿哲', '怡君', '大文']

刪除可以用 remove()(依「值」刪)、pop()(依「索引」刪並回傳該值):

members.remove("阿哲")    # 刪掉值為 "阿哲" 的元素
last = members.pop()      # 拿掉最後一個並回傳
print(last)               # 輸出:大文
print(members)            # 輸出:['小芸', '佳玲', '怡君']

修改就直接用索引賦值:

members[0] = "曉芸"
print(members)            # 輸出:['曉芸', '佳玲', '怡君']

查詢成員是否存在、在哪個位置:

print("佳玲" in members)        # 輸出:True
print(members.index("佳玲"))    # 輸出:1

切片(slicing):一次取一段

切片是 Python 很優雅的設計,語法是 串列[起點:終點:間隔]包含起點、不包含終點

nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(nums[2:5])     # 輸出:[2, 3, 4]
print(nums[:3])      # 輸出:[0, 1, 2](起點省略 = 從頭)
print(nums[7:])      # 輸出:[7, 8, 9](終點省略 = 到尾)
print(nums[::2])     # 輸出:[0, 2, 4, 6, 8](每 2 個取 1 個)
print(nums[::-1])    # 輸出:[9, 8, 7, 6, 5, 4, 3, 2, 1, 0](反轉)

切片會產生一個新的串列,不會動到原本的資料,這在處理資料時很安全。

元組(tuple):建立後就不能改

元組(tuple)長得很像串列,但用小括號 () 建立,而且一旦建立就不能修改(稱為不可變,immutable)。

point = (120.5, 24.3)      # 一個經緯度座標
print(point[0])            # 輸出:120.5
print(point[1])            # 輸出:24.3

# point[0] = 100  # 這行會報錯!TypeError: 'tuple' object does not support item assignment

為什麼需要一個「不能改」的容器?因為有些資料本來就不該被改:座標、RGB 顏色值、一筆固定的設定。把它做成 tuple,等於昭告「這組資料是一體的、別動它」,也能避免不小心改錯。

tuple 還有一個常見用法:多重指派函式回傳多個值

def min_max(data):
    return min(data), max(data)   # 其實回傳的是一個 tuple

low, high = min_max([3, 7, 1, 9, 4])
print(low, high)    # 輸出:1 9

小提醒:建立只有一個元素的 tuple 時,逗號不能省,否則括號只是普通括號。

a = (5)     # 這是整數 5,不是 tuple
b = (5,)    # 這才是只有一個元素的 tuple
print(type(a))   # 輸出:<class 'int'>
print(type(b))   # 輸出:<class 'tuple'>

字典(dict):用「鍵」找「值」

當你想用「名字查電話」「學號查成績」這種對應關係時,字典(dict)就是首選。它儲存的是一組組「鍵值對(key-value pair)」,用大括號 {} 建立。

contacts = {
    "小芸": "0912-345-678",
    "阿哲": "0922-111-222",
    "怡君": "0933-444-555",
}
print(contacts["小芸"])    # 輸出:0912-345-678

注意這裡的差別:list 是用「位置(索引 0、1、2)」找東西,dict 是用「鍵(名字)」找東西。當資料有自然的標籤時,dict 讀起來直覺多了。

新增、修改、刪除

contacts["大文"] = "0944-777-888"   # 鍵不存在 → 新增
contacts["小芸"] = "0900-000-000"   # 鍵已存在 → 修改
del contacts["怡君"]                # 刪除
print(contacts)
# 輸出:{'小芸': '0900-000-000', '阿哲': '0922-111-222', '大文': '0944-777-888'}

安全查詢:用 get() 避免當機

直接用 字典[鍵] 查一個不存在的鍵會直接報錯(KeyError)。比較安全的做法是用 get(),找不到時回傳 None(或你指定的預設值),而不會讓程式爆掉。

print(contacts.get("不存在的人"))          # 輸出:None
print(contacts.get("不存在的人", "查無此人"))  # 輸出:查無此人

走訪(iteration)字典

走訪字典時,請善用 .items(),可以同時拿到鍵和值,這是最 Pythonic 的寫法:

for name, phone in contacts.items():
    print(f"{name} 的電話是 {phone}")
# 輸出:
# 小芸 的電話是 0900-000-000
# 阿哲 的電話是 0922-111-222
# 大文 的電話是 0944-777-888

只想要鍵就用 .keys(),只想要值就用 .values()。另外,要判斷某個鍵在不在,用 in 即可:

print("阿哲" in contacts)   # 輸出:True(in 檢查的是「鍵」,不是值)

集合(set):自動去重與集合運算

集合(set)也用大括號 {} 建立,但它裡面沒有重複元素沒有順序。它最擅長兩件事:去除重複、做集合運算。

一行去重複

raw = [1, 2, 2, 3, 3, 3, 4]
unique = set(raw)
print(unique)          # 輸出:{1, 2, 3, 4}
print(list(unique))    # 轉回 list:[1, 2, 3, 4]

把一個 list 丟進 set(),重複的就自動消失了,比自己寫迴圈判斷簡潔太多。

集合運算:交集、聯集、差集

回到開頭的任務:找出「同時報名 A 跟 B」的人。集合運算就是為這種問題設計的。

activity_a = {"小芸", "阿哲", "怡君", "大文"}
activity_b = {"怡君", "大文", "佳玲"}

print(activity_a & activity_b)   # 交集(兩邊都有):{'怡君', '大文'}
print(activity_a | activity_b)   # 聯集(任一邊有):{'小芸', '阿哲', '怡君', '大文', '佳玲'}
print(activity_a - activity_b)   # 差集(只在 A):{'小芸', '阿哲'}
print(activity_a ^ activity_b)   # 對稱差(只在其中一邊):{'小芸', '阿哲', '佳玲'}

由於 set 沒有順序,上面印出來的元素排列可能跟你看到的不同,這是正常的。

要檢查某個元素在不在 set 裡,一樣用 in,而且這個操作非常快(後面深入段會解釋為什麼)。

何時用哪一個?

四種容器各有定位,選擇時可以這樣判斷:

  • 需要有順序、會增刪改的清單 → 用 list(如報名順序、待辦清單)。
  • 一組固定不該變動的資料 → 用 tuple(如座標、RGB、回傳多值)。
  • 有「鍵 → 值」對應關係 → 用 dict(如名字查電話、學號查成績)。
  • 只在乎「有沒有、要不要去重、做集合運算」 → 用 set(如去重、找交集)。

一個關鍵的差異:判斷某元素是否存在時,in 在 list 上是逐一比對(慢),在 set 與 dict 上卻幾乎是瞬間完成。所以如果你的主要操作是「反覆檢查某筆資料在不在一個大集合裡」,請把它放進 set 或 dict,而不是 list。

動手寫一段:統計文章字詞出現次數

把學到的東西組合起來,寫一個小程式:統計一段文字裡每個字出現幾次,並找出最常出現的字。這裡會同時用到 dict 與 set 的概念。

text = "蘋果 香蕉 蘋果 橘子 香蕉 蘋果 葡萄 橘子 蘋果"
words = text.split()          # 用空白切成 list

# 用 dict 統計次數
counts = {}
for word in words:
    counts[word] = counts.get(word, 0) + 1   # get 的預設值技巧

print("出現過的字種類:", set(words))
print("各字出現次數:", counts)

# 找出出現最多次的字
top_word = max(counts, key=counts.get)
print(f"最常出現的是「{top_word}」,共 {counts[top_word]} 次")

# 輸出:
# 出現過的字種類: {'橘子', '葡萄', '香蕉', '蘋果'}
# 各字出現次數: {'蘋果': 4, '香蕉': 2, '橘子': 2, '葡萄': 1}
# 最常出現的是「蘋果」,共 4 次

這段程式裡,counts.get(word, 0) + 1 是非常經典的計數技巧:第一次遇到某個字時 get 回傳預設值 0,加 1 變成 1;之後每遇到一次就在原本基礎上加 1。set 則幫我們一眼看出「總共有幾種字」。

(補充:標準函式庫 collections.Counter 可以一行完成計數,Counter(words),等你熟悉手寫版本後,再去欣賞它的優雅。)

常見錯誤

  • 誤以為切片包含終點nums[2:5] 取的是索引 2、3、4,不含 5。記住口訣「含頭不含尾」。
  • 在迴圈走訪 list 的同時刪除元素:邊走邊刪會讓索引錯亂、跳過元素。正確做法是用列表生成式建立新清單,例如 result = [x for x in data if x != 0]
  • 字典[鍵] 查可能不存在的鍵:會直接拋出 KeyError 讓程式中斷。不確定鍵在不在時,改用 dict.get(鍵, 預設值)
  • 一個元素的 tuple 忘了加逗號(5) 是整數,(5,) 才是 tuple。這個雷踩過一次就會記得。
  • 指望 set 有順序:set 是無序的,不要假設印出來的順序、也不要用索引 s[0] 去取 set 的元素(會報錯)。需要順序就用 list。

深入探討(研究所視角)

前面我們說 set 與 dict 的查找「非常快」,現在來看看快在哪裡。

dict 與 set 的雜湊(hash)實作

dict 與 set 底層都是雜湊表(hash table)。當你存入一個鍵時,Python 會先對它呼叫 hash() 函式,把鍵轉成一個整數雜湊值,再依這個值算出該放進底層陣列的哪一格(slot)。查找時同樣先算 hash、直接跳到對應的格子,所以平均時間複雜度是 $O(1)$——不管容器裡有 10 筆還是一千萬筆資料,查一筆的時間幾乎不變。

big = set(range(10_000_000))   # 一千萬個元素
print(9_999_999 in big)        # 幾乎瞬間回傳 True

big_list = list(range(10_000_000))
print(9_999_999 in big_list)   # 同樣是 True,但要從頭掃到尾,明顯慢得多

相對地,list 的 in 是線性搜尋,最壞情況要比對到最後一個元素,複雜度是 $O(n)$。這就是為什麼「反覆做存在性檢查」時要選 set 或 dict。

這也解釋了一個限制:只有「可雜湊(hashable)」的物件才能當 dict 的鍵或放進 set。不可變物件(int、str、tuple)都可雜湊;而 list 因為內容會變、雜湊值不穩定,所以不能當鍵或 set 元素:

ok = {("台北", "信義區"): 1}   # tuple 可當鍵,沒問題
# bad = {["台北", "信義區"]: 1}  # TypeError: unhashable type: 'list'

需要補充的是,$O(1)$ 是平均情況。當不同鍵算出相同 slot(雜湊碰撞,hash collision)時,需要額外處理(CPython 採開放定址法 open addressing 探測下一格),最壞情況會退化成 $O(n)$。但在實務上,Python 會在裝填率過高時自動擴充底層陣列,碰撞機率被壓得很低,所以平均 $O(1)$ 是可靠的假設。

list 與 deque 的複雜度差異

list 在 Python 裡是用動態陣列(dynamic array)實作的,元素連續存放在記憶體中。這帶來兩個特性:

  • 用索引存取 lst[i]、在尾端新增 append() 或刪除 pop():平均 $O(1)$。
  • 開頭插入或刪除 insert(0, x)pop(0):$O(n)$。因為陣列開頭一動,後面所有元素都得整體往後或往前搬移。

如果你的演算法需要頻繁在「兩端」進出資料(典型如佇列 queue、廣度優先搜尋 BFS),用 list 的 pop(0) 會很慢。這時應改用 collections.deque(double-ended queue,雙端佇列),它底層是雙向鏈結結構,兩端的新增與刪除都是 $O(1)$。

from collections import deque

dq = deque([1, 2, 3])
dq.appendleft(0)    # 左端加入,O(1)
dq.append(4)        # 右端加入,O(1)
print(dq)           # 輸出:deque([0, 1, 2, 3, 4])
print(dq.popleft()) # 左端取出,O(1),輸出:0
print(dq.pop())     # 右端取出,O(1),輸出:4

代價是 deque 的隨機索引存取 dq[i] 是 $O(n)$(要從一端走過去),所以它不適合「常常用索引跳著取」的場景。這正體現了資料結構的核心思想:沒有萬用的最佳解,只有針對特定操作模式的取捨。當你清楚自己最頻繁的操作是什麼(尾端增刪?兩端進出?存在性檢查?鍵值對應?),自然就知道該選 list、deque、set 還是 dict。

把這套「依操作模式選容器」的思維練熟,你寫出來的程式不只跑得對,還會跑得又快又乾淨。現在,回去把開頭那份社團名單的三個任務各寫一遍吧。

AI 共讀助教正在陪你讀:Python 容器全攻略:list/dict/set 怎麼選
嗨!我是這篇文章的共讀助教,只根據〈Python 容器全攻略:list/dict/set 怎麼選〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。