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

M5 流程控制

M3、M4 的練習逐本書寫一行:四本書還可以,四千本就不行了。for 讓同一段程式對每一項重複執行,if 讓它按條件決定做不做。兩者合起來,就能一次處理整間書店。
約 50 分鐘練習 notebook 1 個先修:M4
學完本單元,你能夠:
  1. 用 if/elif/else 按條件執行不同的程式;
  2. 用 for 走過 list 和 dict,在迴圈裏累加、計數和篩選;
  3. 把「迴圈+條件」改寫成 comprehension,也能把 comprehension 讀回迴圈;
  4. 用兩層迴圈走訪巢狀資料,並改寫 M3、M4 的練習。

1為甚麼需要迴圈 Why loops

M4 的練習要列出全店的標籤,寫了這樣一行:

all_tags = (set(catalog[0]["tags"]) | set(catalog[1]["tags"]) | set(catalog[2]["tags"])
            | set(catalog[3]["tags"]) | set(catalog[4]["tags"]))

它有兩個問題:書多了,這行就要一直加長;書的數目變了,這行就會出錯(少寫一本,或者寫了一本不存在的)。我們真正想說的是:「對每一本書,取出它的標籤」。這就是迴圈要做的事。

flowchart TD
    S([開始]) --> N{"還有下一本書嗎?"}
    N -->|有| T["book = 下一本"]
    T --> C{"book 缺貨嗎?"}
    C -->|是| A["記下書名"]
    C -->|否| N
    A --> N
    N -->|沒有| E([結束])
    classDef q fill:#ffffff,stroke:#d3d1cb,color:#37352f
    classDef s fill:#f1f1ef,stroke:#d3d1cb,color:#37352f
    class N,C q
    class T,A,S,E s
圖 1 一個 for 迴圈加上 if 的流程。每一輪取出下一項,按條件決定做不做;沒有下一項就離開迴圈。

2if:按條件做事 Conditions

stock = 0

if stock > 5:
    print("充足")
elif stock > 0:
    print("少量")
else:
    print("缺貨")

Python 由上而下檢查條件,執行第一個成立的那一段,其餘的跳過。elif(else if 的縮寫)和 else 都可以不寫。

條件可以用 and、or、not 組合:

book = {"title": "看得見的資料", "price": 120, "stock": 3}

book["stock"] > 0 and book["price"] < 150     # True:有貨,而且低於 150 元
not book["stock"] > 0                         # False
縮排就是結構

冒號後面縮排的那幾行,才屬於這個 if。Python 用縮排(本課一律四個空格)判斷哪些程式在條件裏面,不像其他語言用大括號。縮排不一致會報 IndentationError;縮排錯了位置則不一定報錯,只是做錯事。M0 說過 YAML 的縮排就是結構,Python 也一樣。

3for:逐項走過 Loops

先像 M4 練習那樣讀入 catalog.yaml,這一節叫它 books:一個 list,裏面有 4 本書的 dict。

books = yaml.safe_load(Path("catalog.yaml").read_text(encoding="utf-8"))

total = 0
for book in books:
    total = total + book["price"] * book["stock"]

total     # 1135:全店庫存的總值

for book in books: 的意思是:把 books 裏的每一項輪流交給 book 這個名字,每一輪執行一次縮排了的那段程式。第一輪 book 是第一本書,第二輪是第二本,如此類推。

圖 2 逐輪追蹤上面的迴圈。每一輪 book 指向下一本書,total 在上一輪的基礎上加上這本書的庫存值。
圖 2 逐輪追蹤上面的迴圈。每一輪 book 指向下一本書,total 在上一輪的基礎上加上這本書的庫存值。

迴圈前先設 total = 0,迴圈裏每一輪在原本的值上加,這是最常見的累加寫法。total = total + x 也可以簡寫成 total += x。

走過 dict 時,for key in d 拿到的是鍵;要同時拿到鍵和值,用 items()。要編號,用 enumerate()。這兩個每一輪都交出一對值,for key, value in ... 就把這一對拆開,分別交給兩個名字:

for key, value in books[0].items():
    print(key, value)                 # title 看得見的資料 / price 120 / stock 3 / tags [...] / author {...}

for i, book in enumerate(books, start=1):
    print(i, book["title"])           # 1 看得見的資料 / 2 給初學者的 Python …

4在迴圈裏篩選 Filtering in a loop

for 加 if,就能只處理符合條件的項目:

out_of_stock = []
for book in books:
    if book["stock"] == 0:
        out_of_stock.append(book["title"])

out_of_stock     # ['給初學者的 Python']

這個「先準備一個空的 list,符合條件的就 append 進去」的模式,用得非常多,所以 Python 有一個更短的寫法。

5comprehension:一行寫完的迴圈 Comprehensions

上面那四行,可以寫成一行:

out_of_stock = [book["title"] for book in books if book["stock"] == 0]

這叫 comprehension(推導式)。它和迴圈的對應是一一的:

圖 3 迴圈和 comprehension 的對應。同一個編號是同一部分:①要放進結果的值、②走過甚麼、③篩選條件。
圖 3 迴圈和 comprehension 的對應。同一個編號是同一部分:①要放進結果的值、②走過甚麼、③篩選條件。

讀一個 comprehension 時,先讀中間的 for,再讀後面的 if,最後才讀最前面那個值:「對 books 裏每一本 book,如果缺貨,取它的 title」。

不篩選也可以,只是把每一項變換一下:

[book["price"] * 0.9 for book in books]           # [108.0, 162.0, 85.5, 135.0]:九折價
{book["title"]: book["stock"] for book in books}  # 書名 → 庫存的 dict
甚麼時候用哪一種

結果是一個新的 list(或 dict、set),而且一行讀得懂,就用 comprehension。要做好幾件事、要 print、要處理錯誤,或者一行太長,就用普通的迴圈。兩者效果相同,讀得懂比寫得短重要。

6走訪巢狀資料 Looping through nested data

M4 的目錄裏,每本書有一串標籤。要走到每一個標籤,就要兩層迴圈:外層走過每本書,內層走過這本書的每個標籤。

counts = {}
for book in catalog:
    for tag in book["tags"]:
        counts[tag] = counts.get(tag, 0) + 1

counts    # {'python': 2, 'json': 2, 'yaml': 2, '入門': 2, '設定檔': 1, '圖解': 1}

counts.get(tag, 0) + 1:這個標籤之前數過就在原數上加一,第一次見到就由 0 開始。這是用 dict 計數的標準寫法。

7回看 M3、M4 的練習 Revisiting M3 and M4

M4:全店的標籤。 五個 set(...) 變成一行,書再多也不用改:

all_tags = {tag for book in catalog for tag in book["tags"]}

兩個 for 的次序和兩層迴圈一樣:外層在前,內層在後。(catalog.yaml 本身有 4 本書、6 個標籤;M4 練習裏加了一本新書,所以是 7 個。)

M3:新書總值。 原本要逐本書寫一行轉換,再人手跳過庫存未填的那本。用迴圈,規則只寫一次(M3 練習裏這份資料叫 books,這裏改叫 new_arrivals,免得和上面書店目錄的 books 混淆):

total = 0
missing = []
for book in new_arrivals:
    if book["stock"] is None:
        missing.append(book["title"])
        continue                      # 跳過這一本,進入下一輪
    total += float(book["price"]) * int(book["stock"])

total, missing     # (927.0, ['表格與我']):和 M3 的答案一樣

continue 的意思是「這一輪到此為止,直接進入下一輪」。

8容易混淆 Easily confused

這兩樣 分別
迴圈和 comprehension 效果相同;comprehension 只適合「產生一個新容器」,而且要一行讀得懂。
total = x 和 total += x 前者每一輪把 total 換成新值,最後只剩最後一本書的值;後者在原本的值上加。
迴圈裏和迴圈外 縮排了的那幾行每一輪都執行;沒縮排的只在迴圈完結後執行一次。
for k in d 和 for k, v in d.items() 前者只拿到鍵;後者同時拿到鍵和值。
if 和 elif 一串 if 每一個都會檢查;if/elif 只執行第一個成立的。
不要一邊走一邊改同一個 list

在 for book in books: 裏面對 books 做 append 或刪除,迴圈會漏掉或重複某些項目。要篩選,就建立一個新的 list(例如用 comprehension),不要改正在走的那一個。

9動手:書店日報 Hands-on: the bookshop's daily report

  1. 下載 m5-daily-report.ipynb、orders.yaml,以及 M4 的 catalog.yaml,放進 python-course 資料夾;
  2. 執行 uv run jupyter lab,打開 notebook,逐格執行。

小書店今天收到幾張訂單,每張訂單有幾本書。你要用迴圈和條件算出每張訂單的金額、找出訂了缺貨書的訂單、統計每本書賣出多少本,最後寫成一份簡單的日報。

工程線(選做)

m5-sensor-qc-eng.ipynb:用 M4 的 sensors.json,按每種感測器的量程篩出超出範圍的讀數,計算每個感測器的平均值和最大值,並用 comprehension 和普通迴圈各寫一次。

下一步

M6 講函式與模組:把「計算一張訂單的金額」這類步驟包成函式,寫一次,到處用。