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

串列:一排有編號的格子

資料一多,就不可能一個值取一個名字。這一課把一串資料放進一排有編號的格子,並解開新手最常遇到的怪事:明明只改了 b,為什麼 a 也跟著變了?

逐行執行+記憶體動畫 兩個名字共用同一個串列 12 張預測輸出卡

💡 先搞懂問題

假設你在辦公室幫同事訂便當。學過第 2 課的變數(variable,貼在資料上的名字標籤)之後,第一個想法可能是一份取一個名字:order1 = "雞腿飯"、order2 = "排骨飯"、order3 = "魚排飯"。三份還好,可是今天有 23 個人訂,明天又多 2 個人;你想問「總共幾份」「第 5 份是什麼」「把最後一份取消」,每一件事都得一個名字一個名字去改程式。資料一多,這種寫法就撐不下去了。

Python 的解法是串列(list):把一串資料依序放進同一個物件裡,整串只要一個名字。寫法是用方括號把值框起來、中間用逗號隔開,例如 orders = ["雞腿飯", "排骨飯", "魚排飯"]。串列裡的每一個值叫做元素(element,也常叫 item),每個元素都有一個位置編號,叫做索引(index),從 0 開始。第 4 課學過字串的索引,串列用的是同一套規則:orders[0] 是第一份、orders[-1] 是最後一份。

想像茶水間有一排置物櫃,整排掛著一張名牌「orders」,每一格門上漆著編號。要拿第 1 號櫃的東西,報編號就好;1 號櫃換放別的便當,整排櫃子還是同一排;來了新的便當,就在最右邊加裝一格。這一排置物櫃,就是串列。

比喻:茶水間的一排置物櫃 orders 0雞腿飯 1排骨飯 2魚排飯 3咖哩飯 編號從 0 開始;整排只掛一張名牌,報編號就能找到那一格 程式:串列(和播放器的畫法相同) orders = ["雞腿飯", "排骨飯", "魚排飯", "咖哩飯"] orders 串列 list · o1 '雞腿飯'0 '排骨飯'1 '魚排飯'2 '咖哩飯'3 -4-3-2-1
圖 1 一排置物櫃 vs 串列。左邊是名字(標籤),右邊是真正的資料(物件),箭頭表示名字貼在哪個物件上。格子下方的紫色數字是索引,橘色是負索引:-1 永遠是最後一格。

到這裡都還算直覺。新手真正卡住的,是下面這種情況:你把媽媽的購物清單「交給」爸爸,寫成 dad = mom,接著在 dad 加了一項「吐司」,結果印出 mom 也多了吐司。很多人第一次看到會以為 Python 出錯了,其實它完全照規則在做事,只是這條規則和「交給別人一份影本」的直覺不同。

回想第 2 課的說法:名字是標籤,不是箱子。dad = mom 的意思是「把 dad 這張標籤也貼到 mom 貼著的那個物件上」,並沒有複製出第二份清單。這件事在整數、字串身上不會出問題,因為它們不能被修改;串列卻可以就地修改(in-place,直接改同一個物件的內容),所以兩張標籤貼在同一個串列上時,從任何一張標籤去改,另一張都看得到。

dad = mom:只多一張標籤 dad = mom.copy():多一個串列 mom dad mom dad 串列 list · o1 '牛奶'0 '雞蛋'1 '吐司'2 dad.append("吐司") 改的是 o1 這個物件, 所以從 mom 看過去也有吐司。 mom is dad → True 串列 list · o1 串列 list · o2(新的) '牛奶'0 '雞蛋'1 '牛奶'0 '雞蛋'1 '吐司'2 append 只改到 o2,mom 的 o1 不受影響。 mom is dad → False
圖 2 這一課最重要的一張圖。dad = mom 不會複製串列,右邊只有一個物件、兩支箭頭指著它;mom.copy() 才會做出第二個串列。is 問的正是「兩個名字是不是貼在同一個物件上」。

要分清楚的其實只有一件事:這一行是在「改物件」,還是在「動名字」。dad.append("吐司")、dad[0] = "豆漿" 這類寫法是對物件本身動手,貼在那個物件上的每一個名字都會看到變化;dad = ["咖啡"] 則是先做出一個新串列,再把 dad 這張標籤撕下來改貼過去,原本的串列一點都沒變,mom 自然也不受影響。

dad = ["咖啡"]:撕下標籤改貼 mom dad ✕ 串列 list · o1(沒有被動到) '牛奶'0 '雞蛋'1 '吐司'2 串列 list · o2 '咖啡'0 改貼只動 dad 這張標籤,mom 不受影響 改物件本身(共用的名字都看得到) a[i] = 值 a.append(值)a.insert(i, 值) a.pop()a.remove(值) a.sort() 動名字或做出新物件(原物件不變) b = 新的值 b = a.copy()b = a[:] b = sorted(a)b = a[1:3]
圖 3 兩種「改」。黃框的寫法對物件本身動手,所有貼在它上面的名字都會看到;綠框的寫法只動名字,或另外做出新物件交給名字,原本的物件維持原樣。
回到程式:剛才的整排置物櫃對應的是串列物件,門上的編號是索引,掛在整排上的名牌是名字(變數)。比喻有三個地方和真正的程式不同。第一,真實的置物櫃多半從 1 號開始,Python 從 0 開始。第二,置物櫃的數量是固定的,串列卻可以隨時加長、縮短,而且從中間插入或抽掉一格時,後面每一格的編號會自動往前或往後挪。第三,也是最容易誤會的一點:置物櫃是真的把便當「裝」在格子裡,Python 串列的每一格其實放的是「指向某個物件的箭頭」。播放器為了好讀,把數字、字串直接寫在格子裡;等到格子裡放的是另一個串列,你就會看到箭頭從格子裡伸出去,這在第二個實驗室的巢狀串列會很重要。

🎮 互動實驗室一:一格一格看串列怎麼變

按「下一步 ▶」讓 Python 執行一行,再看右邊的記憶體:左欄是名字,右欄是物件,箭頭是「名字貼在哪個物件上」。串列的每一格下方有紫色的索引。遇到 🤔 問題時先選答案,再按下一步看電腦怎麼做(自動播放也會在這裡停下來)。右上角的選單可以切換三段程式:基本操作、排序、超出範圍的錯誤。

畫面說明:在「基本操作」裡,請盯著右邊那個串列的編號 o1:從第 1 行到最後,修改格子、append、insert、pop、remove 全都發生在 o1 身上,編號從頭到尾沒有變,變的只有格子裡的內容(黃色閃一下就是「就地修改」)。第 5 行 insert 之後,注意原本在 0 號的「雞腿飯」被擠到 1 號,後面每一格的索引都加 1。切到「排序」時,重點在第 4 行與第 6 行:sort() 排的是原本的 o1,交回來的卻是 None;最後一步 prices = prices.sort() 讓 prices 改貼到 None,排好的串列沒有任何名字指著,變成虛線的「沒有名字指向它了」。

🎮 互動實驗室二:兩個名字、一個串列

這是整個零基礎系列最重要的一段記憶體動畫。同一段購物清單程式有三種寫法,只差第 2 行:dad = mom、dad = mom.copy()、dad = mom[:]。先用預設的第一種從頭走一遍,再用右上角的選單切換另外兩種,比較右邊的物件數量和箭頭。播放器下方的提示框會一直告訴你:此刻 mom 和 dad 是不是貼在同一個物件上。

畫面說明:選「dad = mom」時,第 2 行執行完右邊沒有出現新物件,只多了一支從 dad 出發的箭頭,兩支箭頭指著同一個 o1;第 3 行 append 讓 o1 閃黃,說明欄會提醒「mom、dad 指向同一個物件」。第 7 行 dad = ["咖啡"] 是改貼:紫色的新箭頭從 dad 指向新串列,mom 的箭頭動也沒動。換成 copy() 或 [:] 時,第 2 行就會多出一個新的串列物件,之後的 append 只有 dad 那一個在閃。

再往下一層:串列裡放串列

串列的格子可以放任何物件,包括另一個串列,這叫巢狀串列(nested list),常用來表示表格:外層每一格是一列,內層是那一列的欄位。下面的程式把「總店菜單」做成兩格,第 0 格是飲料、第 1 格是甜點,再用 copy() 複製給分店。注意看右邊:外層格子裡畫的是小圓點,箭頭從圓點伸出去,指向內層串列。copy() 只會做出新的外層,格子裡的箭頭照抄,這就是淺複製(shallow copy)。切換到第二種寫法,看看把內層也各自複製之後有什麼不同。

畫面說明:第 3 行執行完,數一數右邊的串列物件:淺複製只多了 1 個(分店的外層),兩個外層的格子卻指向同樣的兩個內層串列。第 4 行透過 branch[1] 加上鬆餅,被改到的是共用的甜點串列,所以總店也有鬆餅;第 5 行 branch[0] = ["果汁"] 換掉的是分店外層的格子,總店的外層沒有被碰到,飲料還是咖啡、紅茶。

🎮 互動實驗室三:先猜再看,12 張預測輸出卡

每張卡是一小段程式。先在腦中畫出名字、物件和箭頭,再選你認為會印出的結果;選完會顯示 Python 3.13 實際執行的輸出和原因。選項順序每次重來都會打亂。

第 1 / 12 張答對 0
畫面說明:卡片上的輸出都照 Python 終端機的格式:串列印出來用單引號包字串,例如 ['狗', '貓'];出錯時只列最後一行的錯誤類型與說明,完整的錯誤畫面在下方「原理補完」。答錯的卡,回到實驗室一或二找一段類似的程式,用播放器走一次,通常就會看到自己漏掉的那支箭頭。

📘 原理補完

1. 正式名稱

Python 官方文件把串列(list)歸類為可變序列(mutable sequence):「序列」代表元素有固定的先後順序、可以用索引取值,字串和元組也是序列;「可變」代表建立之後還能就地修改內容,這是串列和字串、元組最大的差別。串列可以放任何型別的物件,同一個串列裡混放數字和字串也可以,不過實務上通常放同一類資料,處理起來比較單純。

orders.append("咖哩飯") 這種「物件後面接一個點,再接名稱和括號」的寫法,是在呼叫這個物件的方法(method)。方法就是屬於某種物件的函式:append、insert、pop、remove、sort、copy 都是串列的方法,只有串列能這樣用;len()、sorted() 則是內建函式(built-in function),把串列當成括號裡的材料交給它。函式怎麼自己寫,第 9 課會講。

2. 常用操作一覽

看懂一個串列操作,要問三件事:它會不會改到原本的串列、它交回來(回傳,return)什麼、什麼情況下會出錯。下表的 a 是一個串列,i 是索引,v 是一個值。

寫法做什麼改到原串列?交回來的值常見錯誤
a[i]取出索引 i 的元素,-1 是最後一個不會那個元素IndexError
a[i] = v把第 i 格換成 v會(指派,不交回值)IndexError
a.append(v)加到最後面會None—
a.insert(i, v)插到索引 i,後面的元素編號 +1會Nonei 太大不會出錯,直接加在最後
a.pop()/a.pop(i)拿走最後一個/索引 i 的元素會被拿走的元素IndexError(空串列或超出範圍)
a.remove(v)刪掉第一個等於 v 的元素會NoneValueError(找不到)
len(a)元素個數不會整數—
v in a有沒有等於 v 的元素不會True/False—
a[i:j]切片:索引 i 到 j-1不會新串列超出範圍不報錯,自動截斷
a.sort()由小到大排好會None數字與字串混在一起會 TypeError
sorted(a)排好放進新串列不會新串列同上
a.copy()/a[:]淺複製不會新串列—

insert 和 pop(i) 有一個容易忽略的副作用:從中間插入或拿走一格,後面所有元素的索引都會跟著變。如果你先記下「雞腿飯在 0 號」,接著在最前面插了一份素食便當,再用 0 號去拿,拿到的已經是素食便當了。

q = ["A", "B", "C"] q.insert(1, "X") q.pop(0) 'A'0 'B'1 'C'2 'A'0 'X'1 'B'2 'C'3 'X'0 'B'1 'C'2 B、C 往後挪,編號 +1 拿走 'A',全部編號 -1 黃色的索引表示「這一格的編號和上一步不一樣了」
圖 4 insert 與 pop(i) 會讓後面元素的索引移動。append 與不帶索引的 pop() 只動最後一格,前面的編號都不變。

3. sort() 與 sorted():一個改原物件,一個做新物件

串列的方法有一個共同的習慣:會就地修改串列的方法,回傳值都是 None(None 是 Python 表示「沒有值」的特殊物件)。append、insert、remove、sort 都是這樣。設計的用意是提醒你「東西已經改在原本的串列上了,不要再去接回傳值」。偏偏新手很常寫 prices = prices.sort(),等於把 prices 改貼到 None,排好的串列反而沒有名字可以找到。想要「原本的不動、另外拿一份排好的」,就用內建函式 sorted()。兩者都可以加 reverse=True 改成由大到小。

prices.sort():排的是同一個 o1 sorted(prices):做出新的 o2 prices prices ranked 回傳值 串列 list · o1 NoneNone 串列 list · o1(不變) 串列 list · o2(新的) 8599120 1208599 8599120 陷阱:prices = prices.sort() 先排好 o1,再把 prices 改貼到回傳值 None;排好的 o1 沒有名字指著,找不回來了
圖 5 sort() 改的是原物件、交回 None;sorted() 原物件不動、交回一個新串列。要接回傳值,就用 sorted()。

4. 切片會做出新串列

第 4 課的字串切片規則在串列上完全一樣:a[start:stop:step] 從 start 取到 stop 的前一格,省略 start 代表從頭、省略 stop 代表到尾,超出範圍不會報錯。差別在於串列可以修改,所以「切片得到的是新物件」這件事變得很重要:改切片出來的串列,不會影響原本的串列。a[:] 是「從頭切到尾」,結果就是一份淺複製,效果和 a.copy() 相同。

s t 串列 list · o1 50 61 72 83 切口 1切口 3 串列 list · o2(新的) 60 71 t = s[1:3] 不含切口 3 之後的 8 t[0] = 0 不會動到 s
圖 6 把索引想成「格子之間的切口」:s[1:3] 從切口 1 切到切口 3,剛好拿到兩格。切下來的是一個新串列,索引重新從 0 算起。

5. 共用、複製與淺複製

整理成三條規則就夠用了。第一,= 永遠不會複製,只是讓左邊的名字貼到右邊算出來的物件上。第二,想要一份互不影響的串列,用 a.copy()、a[:] 或 list(a),三者都會做出新的外層串列。第三,這些都是淺複製:新串列的格子照抄原本的箭頭,所以格子裡如果是另一個串列,兩邊仍然共用那個內層串列。格子裡是數字或字串時不會有問題,因為它們不能被修改,共用也無妨。

要連內層一起分開,最直接的寫法是把每個內層也各自複製,就像實驗室二的第二種寫法;以後學到 import(模組的匯入)之後,也可以用標準函式庫的 copy.deepcopy() 一次複製到底。

menu branch 外層 list · o1 外層 list · o4(新的) 0 1 0 1 內層 list · o2(共用) 內層 list · o3(共用) '咖啡' '紅茶' '蛋糕' '鬆餅' branch = menu.copy():外層是新的 o4,格子裡的箭頭照抄,內層 o2、o3 兩邊共用。 所以 branch[1].append("鬆餅") 改到 o3,從 menu[1] 看過去也有鬆餅。
圖 7 巢狀串列與淺複製。灰色箭頭是 menu 外層格子的參照,紫色虛線是 branch 外層格子的參照:兩組箭頭最後指向同樣的內層串列。

6. 元組 tuple:不能修改的串列

元組(tuple)用圓括號建立,例如 point = (25.03, 121.56)。它和串列一樣有順序、可以用索引和切片、可以 len() 和 in,但建立之後不能修改:沒有 append、remove、sort 這些方法,point[0] = 0 會得到 TypeError。適合放「一組固定在一起、不該被改動」的值,例如經緯度、日期的年月日。只有一個元素的元組要寫逗號 (5,),因為 (5) 只是加了括號的整數 5。

不能修改還有一個好處:元組可以當作下一課字典的「鍵」,串列不行,原因第 6 課會說明。要提醒的是,元組的「不能修改」只管它自己的格子:如果元組的某一格指向一個串列,那個串列本身仍然可以 append。

串列 list [ ] 可以修改 元組 tuple ( ) 建立後不能改 '回信' '繳費' + 新增 25.03 121.56 todo[0] = "回電話" ✔todo.append("買菜") ✔ 適合:會增減、會排序的資料 point[0] = 0 ✘ TypeErrorpoint[0]、len(point) ✔ 適合:座標、固定的一組值
圖 8 串列與元組都有順序、都能用索引取值;差別在能不能修改。

7. 常見錯誤長什麼樣子

以下是 Python 3.13 實際執行的錯誤畫面(在你的電腦上,File 後面會是完整的檔案路徑)。讀法是從最後一行往上看:最後一行告訴你錯誤類型與原因,上面指出是哪一行,~ 與 ^ 標出出問題的那一段。第 10 課會專門練習讀錯誤訊息。

讀取超出範圍:三個元素的串列,索引只到 2。

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(scores[3])
          ~~~~~~^^^
IndexError: list index out of range

指派超出範圍:串列不會因為你指派到不存在的格子就自動加長,要加長請用 append。注意訊息多了 assignment 這個字。

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    box[3] = 4
    ~~~^^^
IndexError: list assignment index out of range

remove 找不到:remove 依值刪除,值不存在就報 ValueError。不確定有沒有時,可以先用 in 檢查(搭配第 7 課的 if)。

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    tags.remove("java")
    ~~~~~~~~~~~^^^^^^^^
ValueError: list.remove(x): x not in list

修改元組:

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    point[0] = 5
    ~~~~~^^^
TypeError: 'tuple' object does not support item assignment

接了 sort() 的回傳值:todo = todo.sort() 之後 todo 已經是 None,下一行再 append,錯誤訊息說「NoneType 物件沒有 append 這個屬性」。看到 NoneType,先回頭找是不是哪裡接了一個回傳 None 的方法。

Traceback (most recent call last):
  File "main.py", line 3, in <module>
    todo.append("洗衣")
    ^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'append'

另外,對空串列 pop() 會得到 IndexError: pop from empty list。

8. 兩段完整範例

第一段是待辦清單,把這一課的基本操作串起來;註解裡的輸出都是實際執行的結果。

todo = ["回信", "訂會議室"]           # 建立串列:兩個待辦事項
todo.append("繳電話費")               # append:加到最後面
todo.insert(0, "交週報")              # insert:插到索引 0,其他項目往後挪一號
print(todo)                           # ['交週報', '回信', '訂會議室', '繳電話費']
print(len(todo), todo[1], todo[-1])   # 4 回信 繳電話費
done = todo.pop(0)                    # pop(0):拿走索引 0 的項目,並把它交回來
print("完成:", done)                 # 完成: 交週報
todo.remove("訂會議室")               # remove:依「值」刪除第一個符合的項目
print("訂會議室" in todo)             # False:in 檢查串列裡有沒有這個值
first = todo[0:1]                     # 切片:做出一個新串列
print(first, todo)                    # ['回信'] ['回信', '繳電話費']

第二段把共用、複製、淺複製和元組放在一起,建議先自己預測每個 print,再對照註解。

base = ["白飯", "青菜"]               # 套餐的基本配菜
lunch = base                          # 沒有複製:兩個名字貼在同一個串列上
lunch.append("滷蛋")                  # 就地修改這個共用的串列
print(base)                           # ['白飯', '青菜', '滷蛋']:base 也看得到
dinner = base.copy()                  # copy():做出另一個新串列
dinner.remove("滷蛋")                 # 只改到 dinner 那一個
print(base, dinner)                   # ['白飯', '青菜', '滷蛋'] ['白飯', '青菜']
print(lunch is base, dinner is base)  # True False
table = [[1, 2], [3, 4]]              # 巢狀串列:外層 2 格,各指向一個內層串列
backup = table.copy()                 # 淺複製:只做出新的外層
backup[0][0] = 99                     # 改的是兩邊共用的內層串列
print(table)                          # [[99, 2], [3, 4]]:table 也變了
point = (25.03, 121.56)               # 元組 tuple:建立後不能修改
print(point[0], len(point))           # 25.03 2

9. 以後會在哪裡遇到

讀 AI 或資料分析的範例程式時,串列到處都是:一批要送進模型的句子、一串模型的預測結果、二維的巢狀串列當成一張小表格。NumPy 的陣列和 pandas 的表格也沿用索引與切片的寫法,但有一點正好相反:NumPy 陣列的切片不會複製資料,改切片會改到原陣列,地圖上的「索引、切片與 view/copy」節點有詳細說明。不管換到哪個套件,先問「這一行是在改物件,還是做出新物件」,就不容易被嚇到。

✅ 自我檢測

6 題,每題的答案都用 Python 3.13 實際執行確認過。選完立刻顯示對錯與解析,全部作答後會出現總分。目前得分:0 / 6

Q1.下列程式印出什麼?

items = ["筆", "尺", "膠帶"]
print(items[-2], len(items))
-1 是最後一個(膠帶),-2 是倒數第二個(尺)。len 算的是元素個數 3,和最大索引 2 不同。

Q2.執行下列程式,結果是什麼?

box = [1, 2, 3]
box.append(4)
box[4] = 5
print(box)
append 之後 box 有 4 個元素,索引是 0~3。box[4] = 5 想指派到不存在的第 4 格,串列不會自動加長,所以報 IndexError,第 4 行的 print 不會執行。要加在最後請用 append。

Q3.下列程式印出什麼?

a = [1, 2, 3]
b = a
a = [7, 8]
print(b)
第 2 行讓 b 貼到同一個 [1, 2, 3]。第 3 行是「改貼」:做出新串列 [7, 8],把 a 撕下來貼過去,原本的串列沒有被修改,b 仍貼在它上面。會互相影響的是 append 這類就地修改,不是重新指派。

Q4.下列程式印出什麼?

x = [5, 2, 8]
x = x.sort()
print(x)
sort() 把原串列排成 [2, 5, 8],但它的回傳值是 None;第 2 行把 x 改貼到這個 None,排好的串列就沒有名字指著了。想接回排好的結果,改寫成 x = sorted(x),或只寫 x.sort() 不要接。

Q5.下列程式印出什麼?

t = [[0, 0], [0, 0]]
u = t[:]
u[1][0] = 7
print(t)
t[:] 是淺複製,只做出新的外層;u[1] 和 t[1] 指向同一個內層串列,改它的第 0 格,從 t 看也會變。選 A 的人把 [:] 想成了「連內層一起複製」。

Q6.下列程式印出什麼?

nums = [3, 1, 2]
first = nums[0:1]
nums.sort()
print(first, nums[0])
切片 nums[0:1] 在排序前就做出了新串列 [3](切片的結果一定是串列,所以印出來有方括號)。之後 sort() 只改 nums 這個物件,first 不受影響;排好後 nums[0] 是 1。