Python 資料全景課 / 第二部分 Python 基礎 · M4

M4 容器

一本書有書名、價錢、作者;一間書店有很多本書。容器把很多個值組織起來。JSON 和 YAML 讀入 Python 之後,就是一層套一層的容器。
約 50 分鐘練習 notebook 1 個先修:M3
學完本單元,你能夠:
  1. 用 list 和 dict 存放資料,並用索引和鍵取出、修改其中的值;
  2. 沿着一層層的容器,寫出取得某個值的路徑,例如 book["author"]["name"];
  3. 說出 tuple 和 set 各自適合甚麼,以及它們和 JSON 的關係;
  4. 解釋為甚麼 b = a 之後修改 b,a 也會跟着改變。

1容器:把很多個值放在一起 Containers

M3 的值都是單獨一個:一個價錢、一個書名。真實的資料總是一組一組的,容器就是把很多個值放在一起的值。Python 最常用的容器有四種:

容器 寫法 特點 JSON 的對應
list(串列) ["python", "json", "yaml"] 有次序,可以修改 陣列 [ ]
dict(字典) {"title": "看得見的資料", "pages": 120} 用鍵找值,可以修改 物件 { }
tuple(元組) (22.3, 114.1) 有次序,不能修改 沒有,寫出時變成陣列
set(集合) {"python", "json"} 沒有次序,不重複 沒有,不能直接寫出

前兩種最重要:JSON 和 YAML 讀入 Python 之後,只會出現 list 和 dict。

2list:有次序的一串 Lists

list 裏的每一項都有一個位置,叫做索引(index),由 0 開始數:

tags = ["python", "json", "yaml"]

tags[0]          # 'python':第一項
tags[-1]         # 'yaml':負數由尾數起,-1 是最後一項
tags[1:3]        # ['json', 'yaml']:由第 1 項到第 3 項之前
len(tags)        # 3
"json" in tags   # True

list 可以修改:

tags.append("csv")    # 在尾加一項
tags[0] = "Python"    # 修改第一項
第三項是 tags[2]

索引由 0 開始,所以三項的 list 只有 tags[0]、tags[1]、tags[2]。用 tags[3] 會報 IndexError: list index out of range:索引超出範圍。

3dict:用名字找值 Dictionaries

dict 存的是一對一對的「鍵:值」。用鍵(key)來找值,不用記它排第幾:

book = {"title": "看得見的資料", "pages": 120}

book["title"]             # '看得見的資料'
book["price"] = 120       # 加一個新的鍵,或者修改已有的鍵
"pages" in book           # True:檢查有沒有這個鍵
list(book.keys())         # ['title', 'pages', 'price']

用一個不存在的鍵,會報 KeyError:

book["isbn"]   →   KeyError: 'isbn'

資料裏有些欄位不一定有(例如有些書沒有 ISBN),用 get() 比較安全:鍵不存在時,它回傳 None 或你指定的預設值,而不是報錯。

book.get("isbn")          # None
book.get("isbn", "未有")   # '未有'

4巢狀:JSON 的樣子 Nesting

容器裏面可以再放容器。M0 那本書讀入 Python 之後,就是一個 dict,裏面有一個 list 和另一個 dict:

圖 1 M0 的書讀入後的結構。最外層是 dict;tags 是 list;author 是另一個 dict。要取得作者的名字,由外而內逐層寫下鍵和索引。
圖 1 M0 的書讀入後的結構。最外層是 dict;tags 是 list;author 是另一個 dict。要取得作者的名字,由外而內逐層寫下鍵和索引。

這裏的 book 換成了 M0 的 book.json 讀入後的那個 dict,不是上一節那個只有 title、pages、price 的小 dict(用它寫 book["author"] 會報 KeyError)。由外而內,每進一層就加一對方括號:

book = json.loads(Path("book.json").read_text(encoding="utf-8"))   # M0 的書

book["author"]            # {'name': '陳大文', 'email': None}
book["author"]["name"]    # '陳大文'
book["tags"][0]           # 'python'

一間書店的庫存,就是一個 list,裏面每一項是一本書的 dict。這是最常見的資料形狀:

books = [
    {"title": "看得見的資料", "price": 120, "stock": 3},
    {"title": "給初學者的 Python", "price": 180, "stock": 0},
]

books[1]["title"]     # '給初學者的 Python'
len(books)            # 2:有幾本書

讀路徑的方法:books[1]["title"] 由左至右讀作「books 的第 1 項(即第二本書)的 title」。

5tuple 和 set Tuples and sets

tuple 和 list 很像,但建立後不能修改,適合放一組固定在一起的值,例如座標:

position = (22.3, 114.1)
position[0]        # 22.3
position[0] = 0    # TypeError: 'tuple' object does not support item assignment

set 只記「有沒有」,自動去掉重複的值,而且沒有次序(印出來的次序每次可能不同):

set(["python", "json", "python"])          # {'json', 'python'}
{"python", "json"} | {"yaml", "json"}      # 合併:{'json', 'python', 'yaml'}

JSON 沒有這兩種容器:tuple 寫成 JSON 時變成陣列,讀回來就是 list;set 不能直接寫成 JSON,要先用 list() 轉換。

選哪一種容器,可以這樣問:

flowchart LR
    Q1{"要用名字找值嗎?"} -->|是| D["dict"]
    Q1 -->|否| Q2{"只關心有沒有、<br/>不要重複?"}
    Q2 -->|是| S["set"]
    Q2 -->|否| Q3{"建立後還會修改嗎?"}
    Q3 -->|會| L["list"]
    Q3 -->|不會| T["tuple"]
    classDef q fill:#ffffff,stroke:#d3d1cb,color:#37352f
    classDef a fill:#f1f1ef,stroke:#d3d1cb,color:#37352f
    class Q1,Q2,Q3 q
    class D,S,L,T a
圖 2 選擇容器。由左至右回答問題;JSON 和 YAML 讀入的資料只會是 list 和 dict。

6同一個容器,兩個名字 One container, two names

M3 說過,變數只是貼在值上的名字。這件事在容器上有一個重要的後果:

a = ["python"]
b = a              # b 和 a 貼在同一個 list 上
b.append("json")
a                  # ['python', 'json']:a 也「變」了

b = a 沒有複製出第二個 list,只是給同一個 list 多貼了一個名字。透過任何一個名字修改它,用另一個名字看到的也是修改後的樣子。

圖 3 b = a 只是多貼一個名字;a.copy() 才會複製出一個新的 list。改動 c 不影響 a。
圖 3 b = a 只是多貼一個名字;a.copy() 才會複製出一個新的 list。改動 c 不影響 a。

要一個獨立的副本,用 copy():

a = ["python"]
c = a.copy()
c.append("json")
a                  # ['python']:不受影響

copy() 只複製最外一層。如果 list 裏面放的是 dict(例如書店的 books),副本和原本仍然共用那些 dict:改副本裏某本書的價錢,原本那本也會改。練習的最後一題會讓你親眼看到。

數字和字串不會這樣

數字、字串、tuple 都不能修改,price = price + 1 是把名字改貼到一個新的值上,所以不會影響其他名字。只有 list、dict、set 這些可以修改的容器,才需要留意「兩個名字、一個容器」。

7容易混淆 Easily confused

這兩樣 分別
list 和 tuple 都有次序;list 可以修改,tuple 不能。
d["k"] 和 d.get("k") 鍵不存在時,前者報 KeyError,後者回傳 None(或指定的預設值)。
IndexError 和 KeyError 前者是 list 的索引超出範圍;後者是 dict 裏沒有這個鍵。
第一項和 [1] 索引由 0 開始,第一項是 [0],[1] 是第二項。
b = a 和 b = a.copy() 前者兩個名字指向同一個容器;後者複製出一個新的容器。
JSON 的陣列/物件和 Python 的 list/dict 一一對應;JSON 沒有 tuple 和 set。

8動手:書店目錄 Hands-on: the bookshop catalogue

  1. 下載 m4-bookshop-catalog.ipynb 和 catalog.yaml,放進 python-course 資料夾;
  2. 執行 uv run jupyter lab,打開 notebook,逐格執行。

小書店把目錄整理成一份 YAML:每本書有書名、價錢、庫存、標籤,以及一個作者的 dict。你要沿着路徑取出資料、處理有些作者沒有電郵的情況、加入一本新書、用 set 列出全店所有標籤,最後親手撞一次「兩個名字、一個容器」。

工程線(選做)

m4-sensors-eng.ipynb 和 sensors.json:三個感測器,以感測器編號為鍵的 dict,每個有一串讀數(dict of list)。把位置轉成 tuple、取出每個感測器的最新讀數,並避免在計算時改動了原始資料。

下一步

M5 講流程控制:用 for 逐項走過一個 list,用 if 按條件篩選,一次處理整間書店的資料,而不用逐本書寫一行。