💡 先搞懂問題
假設你在辦公室幫同事訂便當。學過第 2 課的變數(variable,貼在資料上的名字標籤)之後,第一個想法可能是一份取一個名字:order1 = "雞腿飯"、order2 = "排骨飯"、order3 = "魚排飯"。三份還好,可是今天有 23 個人訂,明天又多 2 個人;你想問「總共幾份」「第 5 份是什麼」「把最後一份取消」,每一件事都得一個名字一個名字去改程式。資料一多,這種寫法就撐不下去了。
Python 的解法是串列(list):把一串資料依序放進同一個物件裡,整串只要一個名字。寫法是用方括號把值框起來、中間用逗號隔開,例如 orders = ["雞腿飯", "排骨飯", "魚排飯"]。串列裡的每一個值叫做元素(element,也常叫 item),每個元素都有一個位置編號,叫做索引(index),從 0 開始。第 4 課學過字串的索引,串列用的是同一套規則:orders[0] 是第一份、orders[-1] 是最後一份。
想像茶水間有一排置物櫃,整排掛著一張名牌「orders」,每一格門上漆著編號。要拿第 1 號櫃的東西,報編號就好;1 號櫃換放別的便當,整排櫃子還是同一排;來了新的便當,就在最右邊加裝一格。這一排置物櫃,就是串列。
到這裡都還算直覺。新手真正卡住的,是下面這種情況:你把媽媽的購物清單「交給」爸爸,寫成 dad = mom,接著在 dad 加了一項「吐司」,結果印出 mom 也多了吐司。很多人第一次看到會以為 Python 出錯了,其實它完全照規則在做事,只是這條規則和「交給別人一份影本」的直覺不同。
回想第 2 課的說法:名字是標籤,不是箱子。dad = mom 的意思是「把 dad 這張標籤也貼到 mom 貼著的那個物件上」,並沒有複製出第二份清單。這件事在整數、字串身上不會出問題,因為它們不能被修改;串列卻可以就地修改(in-place,直接改同一個物件的內容),所以兩張標籤貼在同一個串列上時,從任何一張標籤去改,另一張都看得到。
dad = mom 不會複製串列,右邊只有一個物件、兩支箭頭指著它;mom.copy() 才會做出第二個串列。is 問的正是「兩個名字是不是貼在同一個物件上」。要分清楚的其實只有一件事:這一行是在「改物件」,還是在「動名字」。dad.append("吐司")、dad[0] = "豆漿" 這類寫法是對物件本身動手,貼在那個物件上的每一個名字都會看到變化;dad = ["咖啡"] 則是先做出一個新串列,再把 dad 這張標籤撕下來改貼過去,原本的串列一點都沒變,mom 自然也不受影響。
🎮 互動實驗室一:一格一格看串列怎麼變
按「下一步 ▶」讓 Python 執行一行,再看右邊的記憶體:左欄是名字,右欄是物件,箭頭是「名字貼在哪個物件上」。串列的每一格下方有紫色的索引。遇到 🤔 問題時先選答案,再按下一步看電腦怎麼做(自動播放也會在這裡停下來)。右上角的選單可以切換三段程式:基本操作、排序、超出範圍的錯誤。
sort() 排的是原本的 o1,交回來的卻是 None;最後一步 prices = prices.sort() 讓 prices 改貼到 None,排好的串列沒有任何名字指著,變成虛線的「沒有名字指向它了」。🎮 互動實驗室二:兩個名字、一個串列
這是整個零基礎系列最重要的一段記憶體動畫。同一段購物清單程式有三種寫法,只差第 2 行:dad = mom、dad = mom.copy()、dad = mom[:]。先用預設的第一種從頭走一遍,再用右上角的選單切換另外兩種,比較右邊的物件數量和箭頭。播放器下方的提示框會一直告訴你:此刻 mom 和 dad 是不是貼在同一個物件上。
dad = ["咖啡"] 是改貼:紫色的新箭頭從 dad 指向新串列,mom 的箭頭動也沒動。換成 copy() 或 [:] 時,第 2 行就會多出一個新的串列物件,之後的 append 只有 dad 那一個在閃。再往下一層:串列裡放串列
串列的格子可以放任何物件,包括另一個串列,這叫巢狀串列(nested list),常用來表示表格:外層每一格是一列,內層是那一列的欄位。下面的程式把「總店菜單」做成兩格,第 0 格是飲料、第 1 格是甜點,再用 copy() 複製給分店。注意看右邊:外層格子裡畫的是小圓點,箭頭從圓點伸出去,指向內層串列。copy() 只會做出新的外層,格子裡的箭頭照抄,這就是淺複製(shallow copy)。切換到第二種寫法,看看把內層也各自複製之後有什麼不同。
branch[1] 加上鬆餅,被改到的是共用的甜點串列,所以總店也有鬆餅;第 5 行 branch[0] = ["果汁"] 換掉的是分店外層的格子,總店的外層沒有被碰到,飲料還是咖啡、紅茶。🎮 互動實驗室三:先猜再看,12 張預測輸出卡
每張卡是一小段程式。先在腦中畫出名字、物件和箭頭,再選你認為會印出的結果;選完會顯示 Python 3.13 實際執行的輸出和原因。選項順序每次重來都會打亂。
['狗', '貓'];出錯時只列最後一行的錯誤類型與說明,完整的錯誤畫面在下方「原理補完」。答錯的卡,回到實驗室一或二找一段類似的程式,用播放器走一次,通常就會看到自己漏掉的那支箭頭。📘 原理補完
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 | 會 | None | i 太大不會出錯,直接加在最後 |
a.pop()/a.pop(i) | 拿走最後一個/索引 i 的元素 | 會 | 被拿走的元素 | IndexError(空串列或超出範圍) |
a.remove(v) | 刪掉第一個等於 v 的元素 | 會 | None | ValueError(找不到) |
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 號去拿,拿到的已經是素食便當了。
3. sort() 與 sorted():一個改原物件,一個做新物件
串列的方法有一個共同的習慣:會就地修改串列的方法,回傳值都是 None(None 是 Python 表示「沒有值」的特殊物件)。append、insert、remove、sort 都是這樣。設計的用意是提醒你「東西已經改在原本的串列上了,不要再去接回傳值」。偏偏新手很常寫 prices = prices.sort(),等於把 prices 改貼到 None,排好的串列反而沒有名字可以找到。想要「原本的不動、另外拿一份排好的」,就用內建函式 sorted()。兩者都可以加 reverse=True 改成由大到小。
4. 切片會做出新串列
第 4 課的字串切片規則在串列上完全一樣:a[start:stop:step] 從 start 取到 stop 的前一格,省略 start 代表從頭、省略 stop 代表到尾,超出範圍不會報錯。差別在於串列可以修改,所以「切片得到的是新物件」這件事變得很重要:改切片出來的串列,不會影響原本的串列。a[:] 是「從頭切到尾」,結果就是一份淺複製,效果和 a.copy() 相同。
5. 共用、複製與淺複製
整理成三條規則就夠用了。第一,= 永遠不會複製,只是讓左邊的名字貼到右邊算出來的物件上。第二,想要一份互不影響的串列,用 a.copy()、a[:] 或 list(a),三者都會做出新的外層串列。第三,這些都是淺複製:新串列的格子照抄原本的箭頭,所以格子裡如果是另一個串列,兩邊仍然共用那個內層串列。格子裡是數字或字串時不會有問題,因為它們不能被修改,共用也無妨。
要連內層一起分開,最直接的寫法是把每個內層也各自複製,就像實驗室二的第二種寫法;以後學到 import(模組的匯入)之後,也可以用標準函式庫的 copy.deepcopy() 一次複製到底。
6. 元組 tuple:不能修改的串列
元組(tuple)用圓括號建立,例如 point = (25.03, 121.56)。它和串列一樣有順序、可以用索引和切片、可以 len() 和 in,但建立之後不能修改:沒有 append、remove、sort 這些方法,point[0] = 0 會得到 TypeError。適合放「一組固定在一起、不該被改動」的值,例如經緯度、日期的年月日。只有一個元素的元組要寫逗號 (5,),因為 (5) 只是加了括號的整數 5。
不能修改還有一個好處:元組可以當作下一課字典的「鍵」,串列不行,原因第 6 課會說明。要提醒的是,元組的「不能修改」只管它自己的格子:如果元組的某一格指向一個串列,那個串列本身仍然可以 append。
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))
Q2.執行下列程式,結果是什麼?
box = [1, 2, 3]
box.append(4)
box[4] = 5
print(box)
box[4] = 5 想指派到不存在的第 4 格,串列不會自動加長,所以報 IndexError,第 4 行的 print 不會執行。要加在最後請用 append。Q3.下列程式印出什麼?
a = [1, 2, 3]
b = a
a = [7, 8]
print(b)
Q4.下列程式印出什麼?
x = [5, 2, 8]
x = x.sort()
print(x)
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。