M9 YAML
- 讀寫映射、序列、純量,用縮排表示結構,寫註解和多行字串;
- 認出 YAML 會猜錯型別的值(
NO、on、010、9:30、1.10、1e-3),並用引號避開; - 只用
safe_load,說出為甚麼不用load; - 看懂 YAML 的錯誤訊息,處理多文件和重複的鍵。
1三種東西 Three kinds of thing
YAML 只有三種東西,和 JSON 一一對應:
| YAML | 寫法 | JSON | 讀入 Python 後 |
|---|---|---|---|
| 映射 mapping | title: 看得見的資料 |
物件 { } |
dict |
| 序列 sequence | - python(每項一行,以 - 開頭) |
陣列 [ ] |
list |
| 純量 scalar | 120、看得見的資料、true |
字串、數字、布林、null | str、int、float、bool、None |
M0 那本書就是一個映射,裏面的 tags 是一個序列,author 是另一個映射。YAML 也接受 JSON 式的寫法:tags: [python, json]、author: {name: 陳大文}。事實上,合法的 JSON 幾乎都是合法的 YAML。
2縮排就是結構 Indentation is structure
JSON 用括號表示「屬於誰」,YAML 用縮排。同樣的三行,縮排不同,意思就不同:
規則:
- 只能用空格,不能用 Tab。用了 Tab 會報
found character '\t' that cannot start any token; - 同一層的項目,縮排要完全對齊。例如
c只縮排了 1 格:
a:
b: 1
c: 2
讀入時報的錯是:
ParserError: while parsing a block mapping
in "<unicode string>", line 1, column 1:
a:
^
expected <block end>, but found '<block mapping start>'
in "<unicode string>", line 3, column 2:
c: 2
^
讀 YAML 的錯誤訊息,看最後的 line … column … 和它下面的 ^,它指着出事的位置(第一個 ^ 只是說「問題出在從這裏開始的那一段」):第 3 行的 c 縮排了 1 格,既不屬於 a(要 2 格),也不和 a 同級(要 0 格)。
圖 1 兩份 YAML 都合法,只是意思不同。這是 YAML 最危險的地方:結構錯了,程式照樣執行,只是讀到的資料不對。讀入後印出來看一眼,或者檢查一下 keys()。
3註解與多行字串 Comments and multi-line strings
# 之後到行尾是註解,讀入時會被丟掉(M0 工程線見過)。這是 YAML 適合寫設定檔的主要原因之一:可以在設定旁邊寫明為甚麼這樣設。
長的文字可以分行寫:
description: | # | 保留換行
第一行
第二行
summary: > # > 把換行變成空格
這兩行讀入後
會接成一行
| 讀入後是 '第一行\n第二行\n';> 讀入後是 '這兩行讀入後 會接成一行\n'。
4YAML 替你猜型別 YAML guesses the type
YAML 的值大多不用引號,那它怎知 120 是數字、看得見的資料 是字串?答案是:它按樣子猜。大多數時候猜對,但以下這些會猜錯。全部用本課的環境(Python 3.12、PyYAML 6)實測:
| 你寫的 | 你想要的 | YAML 讀到的 |
|---|---|---|
country: NO(挪威) |
'NO' |
False |
switch: on |
'on' |
True |
zip: 010 |
'010' |
8(當作八進制) |
phone: 0123456 |
'0123456' |
42798(八進制) |
phone: 0912345678 |
'0912345678' |
'0912345678'(有 8、9,不是八進制,所以是字串) |
open: 9:30 |
'9:30' |
570(當作六十進制:9×60+30) |
open: 09:30 |
'09:30' |
'09:30'(以 0 開頭,不當六十進制,所以是字串) |
version: 1.10 |
'1.10' |
1.1 |
lr: 1e-3 |
0.001 |
'1e-3'(字串!要寫 1.0e-3) |
date: 2026-10-03 |
視乎用途 | datetime.date(2026, 10, 3) |
留意兩組:同樣是電話號碼,一個變成數字、一個保持字串,只因為裏面有沒有 8 和 9;同樣是開店時間,9:30 變成數字,09:30 卻是字串。這類錯誤最難發現,因為大部分資料看起來都沒事。
這個課程網站的設定檔 course.yml 裏,本來有一個叫 no 的鍵(「編號」的意思)。建置時程式找 x["no"],卻報了 KeyError: 'no':因為 YAML 把鍵 no 讀成了 False,dict 裏的鍵是 False,不是 'no'。最後把鍵改名為 label 才解決。
5怎樣避開:加引號 The fix: quote it
規則很簡單:不是用來計算的值,就加引號。
flowchart TD
Q1{"這個值要拿來<br/>計算嗎?"} -->|要| N["不加引號<br/>(數字要有小數點或整數)"]
Q1 -->|不要| Q2{"它是編號、代碼、電話、<br/>版本、時間、國家代碼?"}
Q2 -->|是| Y["加引號"]
Q2 -->|不是| Q3{"它像 yes、no、on、off、<br/>true、數字或日期嗎?"}
Q3 -->|像| Y
Q3 -->|不像| P["可以不加"]
classDef q fill:#ffffff,stroke:#d3d1cb,color:#37352f
classDef a fill:#f1f1ef,stroke:#d3d1cb,color:#37352f
class Q1,Q2,Q3 q
class N,Y,P a
country: "NO"
version: "1.10"
phone: "0123456"
open_at: "9:30"
learning_rate: 1.0e-3 # 要計算的,就寫成 YAML 認得的數字
還有兩種情況一定要加引號,否則會直接報錯:值裏面有「冒號加空格」(title: Python: 入門 會報 mapping values are not allowed here),或者值以 # 開頭(會被當成註解,讀到 None)。寫這個單元時,本課的術語表就因為一句「鍵: 值」中了第一種。
反方向,用 Python 寫出 YAML 時,yaml.safe_dump 會自動替 PyYAML 自己會讀錯的字串加引號:
yaml.safe_dump({"country": "NO", "version": "1.10"})
# country: 'NO'
# version: '1.10'
但它只按 PyYAML 的規則(YAML 1.1)判斷。"1e-3"、"0912345678" 在 PyYAML 讀來本來就是字串,所以寫出時不加引號,變成 1e-3、0912345678;換了跟 YAML 1.2 的工具,就會讀成數字(見後面「YAML 1.1 與 1.2」一節)。寫出的檔案要給別的工具讀,這類值就要自己檢查、加上引號。
寫中文時加 allow_unicode=True,否則中文會變成 "\u770B" 這樣的轉義碼(和 M8 的 ensure_ascii 同一回事)。
6safe_load,不要 load Always safe_load
本課一直用 yaml.safe_load()。PyYAML 也有 yaml.load(),配合某些設定時,它能按 YAML 檔裏的指示建立任何 Python 物件,甚至執行命令。打開一份從網上下載的 YAML,就可能被它執行了程式。
safe_load 只會建立 dict、list、字串、數字這些普通的值;遇到這類指示就報錯,不執行:
ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object/apply:os.system'
讀設定檔、讀資料,一律用 yaml.safe_load;寫出用 yaml.safe_dump。看到網上的例子寫 yaml.load(f),自己用時改成 safe_load。
7多文件與重複的鍵 Multiple documents and duplicate keys
一個 YAML 檔可以用 --- 分成幾份文件。safe_load 只讀一份,遇到第二份會報 expected a single document in the stream;要全部讀,用 safe_load_all:
list(yaml.safe_load_all("a: 1\n---\na: 2\n")) # [{'a': 1}, {'a': 2}]
另一個陷阱:同一個映射裏寫了兩次同一個鍵,PyYAML 不會報錯,只保留最後那個:
yaml.safe_load("price: 120\nprice: 150") # {'price': 150}
設定檔很長時,很容易在下面重複寫了一個上面已有的設定,然後奇怪為甚麼改上面那個沒有作用。
8YAML 1.1 與 1.2 YAML 1.1 and 1.2
上面 NO、on、010、9:30 這些猜測,大多來自舊版規格 YAML 1.1。2009 年的 YAML 1.2 取消了它們:布林只剩 true/false,八進制要寫 0o10,也沒有六十進制。
問題是 PyYAML 至今仍然跟 1.1,而其他工具各有各的版本。同一份 YAML,交給不同的程式,可能讀出不同的結果。所以最可靠的做法不是記住哪個工具跟哪個版本,而是上一節的規則:不是用來計算的值,就加引號。
9容易混淆 Easily confused
| 這兩樣 | 分別 |
|---|---|
| YAML 和 JSON | 表示同樣的資料;YAML 用縮排、可寫註解、值多數不用引號,但會猜型別;JSON 嚴格,不猜。 |
safe_load 和 load |
前者只建立普通的值;後者可能按檔案的指示建立任何物件。一律用前者。 |
NO 和 "NO" |
前者在 PyYAML 讀到 False;後者是字串。 |
1e-3 和 1.0e-3 |
前者在 PyYAML 是字串;後者是 0.001。 |
| ` | 和>` |
| 縮排錯誤和結構錯誤 | 前者報錯;後者合法,只是讀到的結構和你想的不同。 |
10動手:App 的設定檔 Hands-on: an app's settings file
- 下載
m9-app-config.ipynb和shop-config.yaml,放進python-course資料夾; - 執行
uv run jupyter lab,打開 notebook,逐格執行。
小書店的網上商店有一份設定檔,列出送貨的國家、營業時間、聯絡電話、版本號等。同事說「挪威的客人一直下不到單」。你要讀入設定檔,逐個檢查每個值的型別,找出所有被 YAML 猜錯的值,加上引號修好,再用一個函式自動檢查型別,以後改設定檔時就不會再犯。
m9-experiment-config-eng.ipynb 和 experiment.yaml:一份實驗參數設定檔。學習率寫成 1e-3 讀到的是字串、韌體版本 1.10 變成 1.1、取樣開始時間 14:00 變成 840、感測器編號 007 變成 7。找出它們,修好,再寫一個用 assert 驗證設定檔的函式。
M10 比較 JSON、YAML、CSV、TOML 各自適合甚麼,並用 JSON Schema 把「這份資料應該長甚麼樣」寫下來,讓程式替你檢查。