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

元組、集合與巢狀資料

一個函式怎麼能「回傳兩個值」?一串有重複的資料要怎麼一次去掉重複?API 交回一大包括號套括號的資料,該從哪裡下手?這一課把這三個困惑一次講清楚。

打包與拆包 集合去重與交集 巢狀資料尋寶

💡 先搞懂問題

學完串列(list,一排有編號的格子)和字典(dict,用鍵查值)之後,開始讀別人寫的程式,很快會撞上三種看不懂的寫法。第一種是 low, high = min_max(weights):等號左邊怎麼有兩個名字?第 9 課說函式用 return 交回結果,難道一次可以交回兩個?第二種是 set(scans):一串掃描紀錄丟進去,出來的東西變短了,印出來的順序還和放進去的不一樣。第三種是 data["boxes"][0]["items"][1]:一行裡接了四組方括號,看起來像密碼。

這三件事分別對應這一課的三個主角:元組(tuple)、集合(set)和巢狀資料(nested data)。元組在第 5 課出現過,就是「用圓括號建立、建立後不能修改的串列」;這一課要補上它最常見、卻最少被說破的用法:打包與拆包。集合是一個新的容器,專門處理「不要重複」和「有沒有」這兩種問題。巢狀資料則不是新型別,而是把前面學過的容器一層套一層放在一起,從網路服務拿到的資料幾乎都長這個樣子。

想像青松物流的倉庫。出貨前,理貨員把「貨號、件數、重量」三樣東西依序放進一個紙箱、封上膠帶,這個動作叫打包;收貨端拆開箱子,照順序把三樣東西放到三個位置,這叫拆包。倉庫門口有一台掃描器,記下「今天有哪些貨號進來過」:同一個貨號掃兩次,名單上還是只有一筆,名單也不管誰先誰後,只負責回答「有沒有」。倉庫裡的東西則是一層套一層的:一張出貨單裡有好幾個紙箱,每個紙箱裡又有好幾件商品,要找某一件,就得說「第 0 箱裡的第 1 件」,從外往內一層一層指定。

比喻:青松物流的倉庫 ① 封箱的紙箱 A12 3 4.5 貨號 件數 重量 拆箱:依序放到三個位置 封好就不能換裡面的東西 ② 掃描器的名單 A12 B07 A12 C33 B07 名單 A12B07C33 同一個貨號只記一筆 只回答「有沒有」 ③ 一層套一層 出貨單 QS-1024 第 0 箱 筆記本 書籤 第 1 箱 桌曆 第 0 箱的第 1 件=書籤
圖 1 同一個倉庫裡的三個場景:封箱的紙箱是元組,掃描器的名單是集合,出貨單裡有箱子、箱子裡有商品,就是巢狀資料。

換成程式,封箱就是寫一串用逗號隔開的值:box = "A12", 3, 4.5。Python 看到逗號,會自動把它們裝進一個元組,括號寫不寫都可以。反過來,等號左邊放好幾個名字、右邊放一個元組,Python 就會依序拆開、一個名字貼一格。所以開頭那一行 low, high = min_max(weights) 的真相是:函式其實只交回一個元組,左邊的兩個名字把它拆開了。

打包:逗號把幾個值裝成一個元組 box = "A12", 3, 4.5 box 元組 tuple · o1 'A12'0 31 4.52 括號可寫可不寫, 真正做出元組的是逗號 拆包:依序貼到左邊的名字 code, qty, weight = box code qty weight 個數要剛好對上(3 格) 左邊 3 個名字 ✔ 左邊 2 個名字 ✘ ValueError
圖 2 打包與拆包。上半部和播放器的畫法相同:名字在左、物件在右,箭頭是「名字貼在哪裡」。下半部的三個名字,貼的就是元組三格裡的那三個物件,沒有複製任何東西。

集合就是那份掃描名單:set(scans) 把串列裡的貨號倒進集合,重複的自動只留一個,所以變短了。集合不記先後,也沒有「第幾個」,它擅長的是回答「某個貨號在不在裡面」,以及「兩份名單有哪些共同的貨號」。

巢狀資料就是出貨單。字典的值可以是一個串列,串列的格子裡又可以放字典,像下圖這樣一層包一層。讀取時從最外層開始,每接一組方括號就往裡走一層:外層是字典就用 ["鍵"],是串列或元組就用 [索引]。開頭那串看起來像密碼的寫法,其實是四個很普通的動作接在一起。

寫出來的樣子 記憶體裡的樣子(和播放器相同) order = { "id": "QS-1024", "items": [ {"sku": "A12", "qty": 2}, {"sku": "C33", "qty": 1}, ], } order 字典 dict · o1 'id' 'QS-1024' 'items' 串列 list · o2 01 字典 dict · o3 'sku''A12' 'qty'2 字典 dict · o4 'sku''C33' 'qty'1 order["items"][1]["qty"] o1 → o2 第 1 格 → o4 的 qty 黃色是這條路徑經過的格子,最後拿到 1。外層格子放的是箭頭,不是把內層整個裝在裡面。
圖 3 巢狀資料的兩種看法。左邊縮排的寫法是給人讀的;右邊是 Python 真正存放的方式:外層格子裡是一個小圓點,箭頭指向另一個物件。
回到程式:剛才封箱的紙箱對應元組,打包是「逗號把幾個值裝成一個元組」,拆包是「左邊幾個名字依序貼到元組的每一格」;掃描器的名單對應集合;出貨單裡有箱子、箱子裡有商品,對應巢狀資料。比喻有三個地方和真正的程式不同。第一,真的紙箱可以拆開重封、換掉內容,元組建立後永遠不能改,想要不同的內容只能做一個新元組、讓名字改貼過去。第二,現實的掃描紀錄通常照時間排列,集合則完全不保證順序,印出來的先後可能和放進去的不同。第三,也是最重要的一點:現實中一件商品只能放在一個箱子裡,Python 的外層容器放的卻是箭頭,所以同一個內層串列可以同時被兩個外層格子指著。第 5 課看過的淺複製陷阱就是這樣來的,實驗室三會在字典裡再看一次。

🎮 互動實驗室一:元組的打包與拆包

按「下一步 ▶」讓 Python 執行一行,看右邊的記憶體怎麼變:左欄是名字,右欄是物件,箭頭是「名字貼在哪個物件上」。遇到 🤔 問題時先選答案再往下走(自動播放也會在這裡停下來)。右上角的選單有四段程式:① 打包與拆包、② 函式交回兩個值、③ 元組當字典的鍵、④ 拆包的個數對不上。

畫面說明:在「① 打包與拆包」裡,第 3 行執行完,右邊沒有多出新的元組,只多了三個名字,各自的箭頭指向原本就在元組裡的那三個物件(元組格子裡的值,和右邊方塊上的值一模一樣)。第 6 行交換之後,注意兩個字串方塊完全沒動,動的是 left 與 right 的箭頭。切到「② 函式交回兩個值」,盯著第 4 行 return 那一步:呼叫框裡出現的「回傳值」指向一個元組,這就是函式交出去的唯一一個東西;回到第 7 行時,它被拆開貼到 low、high 上,元組本身因為沒有名字指著,就不畫了。

🎮 互動實驗室二:集合去掉重複、比對兩份清單

集合用大括號建立,或用 set(...) 把一個串列轉成集合。播放器把集合畫成一排沒有索引的格子:因為集合沒有「第幾個」。另外要先說好一件事:播放器為了方便對照,會把集合的元素排序後再畫出來,真正印出來的順序不一定是這樣,第三段程式會讓你親眼看到差別。右上角選單可以切換:① 去掉重複與 in、② 交集、聯集、差集、③ 沒有順序也沒有索引。

畫面說明:「① 去掉重複」的第 2 行執行完,比較右邊的兩個物件:串列 scans 有 5 格、有索引,集合 unique 只有 3 格、沒有索引。第 5 行再 add 一次 A12,說明欄會告訴你「沒有任何改變」,這正是集合的用處。「② 交集」那段每一行都做出一個新的集合,north 和 south 本身從頭到尾沒有被修改。為了讓輸出畫面每次都一樣,① 和 ② 印多個字串元素時都先用 sorted() 排成串列;字串集合直接 print,每次重新執行程式時順序都可能不同。「③ 沒有順序」用的是整數,輸出畫面印出 {12, 30, 15},既不是放進去的順序,也不是由小到大。

🎮 互動實驗室三:一層一層往下取,箭頭從格子指出去

下面這段程式一步一步組出一張出貨單:外層是字典,"items" 的值是一個串列,串列的每一格又是一個字典。請注意右邊的畫法:外層的格子裡只有一個小圓點,箭頭從圓點伸出去,指向內層的物件。接著用 first = order["items"][0] 替內層的字典另外貼一張標籤,看看透過 first 修改時,從 order 那邊看過去會不會變。

畫面說明:第 2、3 行的 append 改的是 order["items"] 指向的那個串列(它在閃黃),外層字典 order 的格子沒有變:它的 items 格一直指著同一個串列。第 5 行之後,first 的箭頭和串列第 0 格的箭頭指向同一個字典,所以第 7 行透過 first 改成 5,第 8 行從 order 一路走進去,看到的也是 5。

複製巢狀資料:淺複製又出現了

第 5 課看過:copy() 只會做出新的外層,格子裡的箭頭照抄,叫做淺複製(shallow copy)。字典的 copy() 也一樣。出貨單想留一份備份再修改時,這件事就會咬人。右上角選單可以切換兩種寫法:① 只用 order.copy()、② 再把裡面的串列也各自複製一份。下方的提示框會一直告訴你,兩邊的 skus 是不是同一個串列。

畫面說明:選「① 只用 copy()」時,第 2 行執行完右邊多了一個新字典,可是兩個字典的 skus 格子都指向同一個串列;第 3 行改 backup["id"] 只換掉 backup 自己的格子,order 不受影響,第 4 行 append 卻改到兩邊共用的串列,所以最後印出的 order 也多了 D01。選「② 兩層都複製」,第 3 行讓 backup 的 skus 格子改指向一個新串列,之後的 append 就只有 backup 看得到。

🎮 互動實驗室四:巢狀資料尋寶

青松物流的一張出貨單 shipment,四種容器都用上了:字典、串列、元組,還有一個集合。下方的「資料樹」把它一層一層攤開,每一列就是一格。有兩種玩法:「🔍 自由探索」點樹上的任何一格,右邊會組出取到這一格的 Python 寫法,並逐層解釋每一組方括號做了什麼;「🎯 尋寶挑戰」會給你一行取值的寫法,請在樹上點出它會拿到哪一格,或者判斷它會出錯。所有結果都和 Python 3.13 實際執行一致。

畫面說明:樹上綠色字的是字典的鍵,紫色的 [0]、[1] 是串列或元組的索引;每一列右邊的小標籤是那一格的型別。集合底下的元素沒有索引,點了也組不出 [...] 的寫法,只能用 in 檢查或用 for 一個一個拿出來。挑戰卡答錯時,綠框會標出正確的那一格,或告訴你 Python 實際丟出的錯誤。

📘 原理補完

1. 正式名稱

Python 官方文件把元組(tuple)歸類為不可變序列(immutable sequence):「序列」代表有順序、能用索引取值,「不可變」代表建立後不能修改。官方教學把 t = 12345, 54321, "hi" 這種寫法叫做元組打包(tuple packing),反過來 x, y, z = t 叫做序列拆包(sequence unpacking)。注意後者的名稱是「序列」拆包:右邊不一定要是元組,串列、字串也能拆,例如 a, b = [1, 2]、x, y = "OK"。

集合(set)在官方文件裡的定義是「沒有順序、元素不重複的一群物件」(an unordered collection with no duplicate elements),常見用途是檢查有沒有某個元素、去掉重複,以及做交集、聯集、差集這類運算。集合是可變物件,可以 add、remove;它的元素則和字典的鍵一樣,必須是不可變的值。

巢狀資料(nested data)不是一種新的型別,而是「容器裡放容器」的統稱:串列裡放字典、字典的值是串列、元組裡放元組……要套幾層都可以,英文常叫 nested data 或 nested data structures。

2. 四種容器放在一起比

到這一課為止,Python 內建的四種容器都見過了。選哪一種,看三個問題就夠了:要不要用位置取值、建立後會不會改、允不允許重複。

容器寫法空的怎麼寫順序可以修改?重複怎麼取值常見用途
串列 list[1, 2, 2][]有,照放入順序可以允許a[i]會增減、要排序的一串資料
元組 tuple(1, 2) 或 1, 2();一個元素寫 (5,)有不行允許t[i]、拆包固定的一組值、函式交回多個值、字典的鍵
字典 dict{"a": 1}{}保持放入順序(3.7 起保證)可以鍵不重複d["鍵"]用名稱查資料、一筆記錄的各個欄位
集合 set{1, 2}set()不保證可以不重複沒有索引;用 in 或 for去掉重複、快速檢查有沒有、比對兩份名單

最容易搞混的是大括號:{} 是空的字典,不是空集合,空集合只能寫 set()。大括號裡有冒號就是字典,只有值就是集合。另一個是一個元素的元組:(5) 只是加了括號的整數 5,(5,) 才是元組,因為做出元組的是逗號。

3. 打包與拆包的規則

打包很單純:一串用逗號隔開的值,就會被裝成一個元組。括號通常可以省略,但有時候必須加,免得 Python 誤會,例如 print(1, 2) 是把兩個值分別交給 print,print((1, 2)) 才是交給 print 一個元組。

拆包只有一條硬規則:左邊的名字數量,要和右邊的元素數量剛好一樣,多了少了都是 ValueError。只想要其中幾格時,習慣用底線 _ 當成「這一格我不需要」的名字。拆包也會出現在 for 迴圈裡:第 6 課的 for name, ext in phone.items():,items() 每一圈交出一個 (鍵, 值) 元組,迴圈把它拆給 name 和 ext 兩個名字。

交換兩個變數 a, b = b, a 是打包和拆包連在一起:Python 一定先把等號右邊整個算完、打包成一個元組,才開始把左邊的名字貼上去。下圖把這三個階段拆開來看。

① 執行前 ② 先算右邊、打包 ③ 拆包、改貼 left right left right '台北' '高雄' '台北' '高雄' 暫時的元組(沒有名字) '高雄'0 '台北'1 left、right 還沒動 兩個字串物件一個都沒有被改、也沒有移動,改變的只有 left、right 兩張標籤貼的位置。
圖 4 left, right = right, left 的三個階段。因為第 ② 步先把右邊的值裝進元組,第 ③ 步改貼 left 時,right 原本指的「台北」已經安全地放在元組裡,不會被弄丟。

函式「回傳多個值」也是同一回事。return low, high 交回的是一個元組,呼叫的地方可以用一個名字整個接住(result = min_max(...),之後用 result[0] 取值),也可以直接拆包(low, high = min_max(...))。兩種寫法都對,拆包通常比較好讀,因為每個值一出來就有自己的名字。

4. 集合:去掉重複、檢查有沒有、比對兩份名單

集合為什麼能自動去掉重複,而且 in 特別快?原因和第 6 課的字典相同:集合會根據元素的內容算出一個號碼(雜湊值,hash),用這個號碼決定元素放在哪一格。加入元素時,先算號碼去看那一格,已經有相同的值就不再放;查詢 x in s 時,也是算一次號碼直接過去看。串列的 in 則要從第 0 格開始一格一格比,資料越多比越久。

串列的 in:一格一格比 集合的 in:算出位置直接看 "E05" in scans A12 B07 C33 D01 E05 F08 12345 從頭比到第 5 格才找到(示意); 找不到的話,每一格都要比過一次。 "E05" in unique 算雜湊值 → 去看第 5 格 C33 A12 F08 E05 B07 D01 集合內部有比元素更多的空位, 每個元素的位置由雜湊值決定; 查詢大多只要看一兩格(示意)。
圖 5 同樣問「E05 在不在裡面」。串列只能從頭找;集合先算出位置再直接過去看,資料再多也差不多快。這也是集合「沒有順序」的原因:元素放在哪一格,看的是雜湊值,不是放進去的先後。

元素的位置由雜湊值決定,所以走訪或印出集合時的順序,和你放進去的順序沒有關係。字串的雜湊值在每次啟動 Python 時還會重新隨機(為了防範特定的攻擊手法),所以同一個字串集合,這次執行印出 {'A12', 'B07'},下次可能變成 {'B07', 'A12'}。需要固定順序時,先用 sorted() 排成串列。同樣的道理,元素必須是不可變的值:把串列加進集合會得到 TypeError: unhashable type: 'list',要放一組值就改用元組。

集合最實用的是下面這幾種運算。符號寫法與方法寫法效果相同,方法寫法讀起來比較像英文句子。

寫法方法寫法意思north = {A12, B07, D01}、south = {B07, C33} 的結果
a & ba.intersection(b)交集:兩邊都有{'B07'}
a | ba.union(b)聯集:任一邊有{'A12', 'B07', 'C33', 'D01'}(印出順序不固定)
a - ba.difference(b)差集:a 有、b 沒有{'A12', 'D01'}
a ^ ba.symmetric_difference(b)對稱差:只出現在其中一邊{'A12', 'C33', 'D01'}
s.add(x)—加入一個元素(已存在就不變)就地修改,回傳 None
s.remove(x)—刪除元素,不存在就 KeyError就地修改
s.discard(x)—刪除元素,不存在也不出錯就地修改
x in s、len(s)—有沒有、有幾個True/False、整數
north south A12D01 B07 C33 兩邊都有 north & south交集 → {B07} north | south聯集 → 四個都有 north - south差集 → {A12, D01} north ^ south對稱差 → 除了 B07
圖 6 集合運算用文氏圖最好懂。交集是重疊的部分,聯集是兩個圓全部,差集是只屬於某一邊的部分。集合本身沒有順序,圖中的排列只是示意。

5. 巢狀資料:一組方括號往裡走一層

讀巢狀取值的寫法,只要記住一件事:每一組方括號,都作用在前一步拿到的那個物件上。所以要從左到右一步一步問「現在手上是什麼型別」:是字典,下一步就用 ["鍵"];是串列或元組,就用 [整數];是集合,就沒有方括號可以用了。用錯了 Python 不會猜你的意思,而是直接報錯,最常見的是對串列用字串去查,得到 TypeError: list indices must be integers or slices, not str。

shipment["boxes"][0]["items"][1] shipment["boxes"][0]["items"][1] 字典4 個鍵 串列2 格(兩箱) 字典第 0 箱 串列2 格(商品) '書籤'字串 字典 → 用鍵查串列 → 用索引字典 → 用鍵查串列 → 用索引 規則:先看「上一步拿到什麼型別」。字典接 ["鍵"],串列和元組接 [整數], 集合不能接方括號。卡住時就從左邊開始,一段一段 print 出來看。
圖 7 一長串方括號其實是一段一段的小動作。上方每一段的顏色,對應下方那一步拿到的物件:先是外層字典,最後拿到字串「書籤」。

很長的取值可以拆成好幾行,每一步替中間結果取個名字,例如 box0 = shipment["boxes"][0],下一行再寫 box0["items"][1]。這樣不會多花記憶體,因為 = 只是多貼一張標籤(實驗室三的 first 就是這樣);但也因為是同一個物件,透過 box0 修改,shipment 裡面也會跟著變。

從網路服務拿到的 JSON 資料,在 Python 裡就是這種字典和串列組成的巢狀資料(第 6 課提過)。JSON 格式本身只有「物件」與「陣列」兩種容器,分別對應字典與串列,沒有元組和集合:元組轉成 JSON 會變成串列,集合則不能直接轉換。怎麼在文字和 Python 物件之間轉換,要用標準函式庫的 json 模組,地圖上的「json 與 csv」節點有說明。

6. 複製巢狀資料:要分開幾層,就複製幾層

第 5 課的結論在字典上完全適用:d.copy()、dict(d)、a.copy()、a[:] 都是淺複製,只做出新的外層,格子裡的箭頭照抄。巢狀資料越深,這個陷阱越容易被忽略,因為外層看起來確實是新的一份。想讓某一層完全分開,就要把那一層也複製一次,像實驗室三的第二種寫法;以後學到 import(匯入模組)之後,標準函式庫的 copy.deepcopy() 可以一次把每一層都複製出來。

order backup 字典 dict · order 的外層 字典 dict · backup(新的) 'id''QS-1024' 'skus' 'id''QS-2048' 'skus' 串列 list(兩邊共用) 'A12' 'C33' 'D01' 要分開這一層 再多寫一行, 把內層也複製: backup["skus"] = order["skus"] .copy() 或匯入 copy 模組 後用 deepcopy() 一次複製每一層 改 backup["id"] 只換掉 backup 自己的格子;append 改的是共用的串列,所以兩邊都看到 D01。
圖 8 字典版的淺複製陷阱。backup 的外層是新的,所以換掉 id 不影響 order;兩個 skus 格卻指向同一個串列,從哪一邊 append,另一邊都看得到。

7. 常見錯誤長什麼樣子

以下是 Python 3.13 實際執行的錯誤畫面(在你的電腦上,File 後面會是完整的檔案路徑)。讀法和第 10 課一樣,從最後一行往上看。

拆包的個數對不上:名字比元素多,訊息是 not enough values;名字比元素少,是 too many values。括號裡寫的 expected 是左邊名字的數量,got 是右邊實際的元素數量。

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    code, qty, weight, note = box
    ^^^^^^^^^^^^^^^^^^^^^^^
ValueError: not enough values to unpack (expected 4, got 3)
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    code, qty = box
    ^^^^^^^^^
ValueError: too many values to unpack (expected 2)

對集合用索引:subscriptable 的意思是「可以用方括號取值」,集合不行。

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(tags[0])
          ~~~~^^^
TypeError: 'set' object is not subscriptable

把串列放進集合:集合的元素和字典的鍵一樣,必須是不可變的值。改成元組 ("A12", "B07") 就可以。

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    seen.add(["A12", "B07"])
    ~~~~~~~~^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'list'

remove 不存在的元素:集合的 remove 找不到時丟出的是 KeyError(不是串列的 ValueError)。不確定有沒有時改用 discard(),它找不到也不會出錯。

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    tags.remove("冷藏")
    ~~~~~~~~~~~^^^^^^^^
KeyError: '冷藏'

以為 {} 是空集合:scans = {} 做出的是空字典,字典沒有 add 方法。

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    scans.add("A12")
    ^^^^^^^^^
AttributeError: 'dict' object has no attribute 'add'

巢狀取值少了一層:order["items"] 是串列,還沒選第幾筆就直接用鍵查。~ 標出的是已經算好的那一段(串列),^ 標出出問題的那一組方括號。

Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(order["items"]["sku"])
          ~~~~~~~~~~~~~~^^^^^^^
TypeError: list indices must be integers or slices, not str

8. 兩段完整範例

第一段把元組與集合串起來;註解裡的輸出都是 Python 3.13 實際執行的結果。

def summarize(weights):                         # 一次交回三個值
    return len(weights), min(weights), max(weights)   # 逗號打包成一個元組

count, light, heavy = summarize([2.5, 1.2, 4.0])  # 拆包:三格貼給三個名字
print(count, light, heavy)                      # 3 1.2 4.0
start, end = "台北", "台中"                      # 右邊打包、左邊拆包
start, end = end, start                         # 交換:右邊先算完才貼
print(start, end)                               # 台中 台北
rates = {("台北", "台中"): 120}                  # 元組不能修改,可以當字典的鍵
print(rates[(end, start)])                      # 120:內容相同就查得到
scans = ["A12", "B07", "A12", "C33"]            # 有重複的掃描紀錄
unique = set(scans)                             # 轉成集合:重複的只留一個
print(len(unique), "B07" in unique)             # 3 True
north = {"A12", "B07"}                          # 另一份貨號集合
print(sorted(unique - north))                   # ['C33']:差集,先排序再印

第二段是一張巢狀的出貨單:一層一層取值、透過另一個名字修改內層,最後確認淺複製時哪一層是共用的。for 迴圈每一圈讓 box 貼到一個箱子字典(第 8 課)。

shipment = {                                    # 外層:字典
    "id": "QS-1024",
    "boxes": [                                  # boxes 的值:串列
        {"code": "A12", "items": ["筆記本", "書籤"]},  # 串列的每一格:字典
        {"code": "B07", "items": ["桌曆"]},
    ],
}
print(shipment["boxes"][0]["items"][1])         # 書籤:四組方括號,一層一層往下
print(len(shipment["boxes"]))                   # 2:箱子串列有兩格
second = shipment["boxes"][1]                   # 替內層字典再貼一張標籤(沒有複製)
second["items"].append("月曆")                  # 透過 second 修改最內層的串列
print(shipment["boxes"][1]["items"])            # ['桌曆', '月曆']:從外層看也變了
for box in shipment["boxes"]:                   # 每一圈,box 貼到一個箱子字典
    print(box["code"], len(box["items"]))       # A12 2,下一圈 B07 2
backup = shipment.copy()                        # 淺複製:只有最外層是新的
print(backup["boxes"] is shipment["boxes"])     # True:箱子串列兩邊共用

9. 以後會在哪裡遇到

讀 AI 與資料分析的範例程式時,這一課的三樣東西幾乎每天出現。拆包:scikit-learn 切分訓練與測試資料的 X_train, X_test, y_train, y_test = train_test_split(X, y),函式交回四份資料,左邊用四個名字一次拆開;NumPy 陣列的 arr.shape 是一個元組,常寫成 rows, cols = arr.shape。巢狀資料:呼叫大型語言模型的 API,交回來的 JSON 常要寫成 reply["choices"][0]["message"]["content"] 才拿得到文字,現在你知道它就是「字典 → 串列 → 字典 → 字典」一層一層往下走。集合:整理文字時建立不重複的詞彙表、比對兩份名單差了哪些項目。

✅ 自我檢測

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

Q1.下列程式印出什麼?

point = 3, 8
x, y = point
x = x + 10
print(point, x)
第 1 行的逗號把 3、8 打包成元組,括號可以省略。第 2 行拆包讓 x 貼到 3;第 3 行算出 13 後只是把 x 改貼過去,元組本身不能修改,也沒有被動到,所以 point 還是 (3, 8)。

Q2.下列程式印出什麼?

tags = {"冷藏", "急件", "冷藏"}
tags.add("急件")
print(len(tags))
建立集合時重複的「冷藏」只留一個,不會出錯;第 2 行加入已經存在的「急件」也什麼都不會發生。集合裡始終只有冷藏、急件兩個元素。

Q3.下列程式印出什麼?

def split_name(full):
    return full[0], full[1:]
result = split_name("林小美")
print(result)
return 後面的逗號把兩個字串打包成一個元組,函式交回的就是這一個元組;result 沒有拆包,所以印出來帶著圓括號。若寫成 last, first = split_name("林小美"),才會拆成兩個名字。

Q4.下列程式印出什麼?

a = {1, 2, 3, 4}
b = {3, 4, 5}
print(sorted(a - b), sorted(a & b))
a - b 是差集:在 a 裡、不在 b 裡的 1、2;a & b 是交集:兩邊都有的 3、4。sorted() 把結果排成串列,所以印出方括號。選 A 的人把差集和對稱差(^)搞混了。

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

shop = {"name": "晴空咖啡", "menu": ["拿鐵", "美式"]}
print(shop["menu"][0])
print(shop["menu"]["拿鐵"])
print(shop["name"][0])
shop["menu"] 拿到的是串列,第 2 行用 [0] 取第一格沒問題。第 3 行對串列用字串去查,串列只接受整數索引,所以是 TypeError(不是 KeyError,KeyError 是字典查不到鍵);程式在這裡停下,第 4 行不會執行。

Q6.下列程式印出什麼?

inv = {"A": [1, 2]}
inv2 = inv.copy()
inv2["A"].append(3)
inv2["B"] = [9]
print(inv)
copy() 是淺複製:inv2 是新的外層字典,但 "A" 格的箭頭照抄,兩邊共用同一個串列,所以 append(3) 從 inv 也看得到。第 4 行新增的 "B" 只加在 inv2 自己的外層,inv 沒有這一格。