> **主要目的:防止錯配 (mis-pairing) 與打錯字 (typo)。**
> 本專案有三條「去尾規則」,每條都是「一個 `regex` 搭配一個 `flag`」。
> 若把 regex 與 flag 拆成兩個獨立參數傳遞,很容易發生:
> – **錯配**:把 `step` 的 regex 卻配上 `processstep` 的 flag
(型別檢查器也擋不住,因為兩者「各自」都合法)。
> – **打錯字**:flag 字串多打一個字母(`…_stepp`),
用一般 `str` 型別完全抓不到。
>
> 解法是兩個型別工具「各司其職」地把錯誤擋在編輯階段:
> – **`NamedTuple`(`StripRule`)**:
把「本該綁在一起的 `regex` 與 `flag`」釘成同一顆物件,
> 呼叫端只傳一顆 rule,配對在定義時就固定 →
**杜絕「regex 配錯 flag」**。
> – **`Literal`(`RmFlagName`)**:
把 flag 的合法值限縮成那三個固定字串,
> 打錯字型別檢查器立刻標紅 →
**杜絕「flag 字串打錯」**。
## 目錄
1. [先回答:`NamedTuple` 到底來自 `typing` 還是 `collections`?](#1)
2. [`collections.namedtuple`:老牌工廠函式](#2)
3. [`typing.NamedTuple`:class 語法 + 型別註記](#3)
4. [兩者關係:其實是同一家族](#4)
5. [本專案為何用 `NamedTuple` 定義 `StripRule`:
讓 `regex` 與 `flag` 一對一綁定,不會錯配](#5)
6. [`Literal`:把「合法值」釘死成幾個常數](#6)
7. [`NamedTuple` + `Literal` 合體:本專案的實戰](#7)
8. [常見誤區](#8)
9. [一頁速查表](#9)
<a id=”1″></a>
## 1. 先回答:`NamedTuple` 到底來自 `typing` 還是 `collections`?
import 看到的是:
from typing import Any, Literal, NamedTuple而不是:
from collections import namedtuple # 注意:小寫、是「函式」**兩個都存在、都能用,但用途/寫法不同:**
關鍵事實:
**`typing.NamedTuple` 底層
就是拿 `collections.namedtuple` 做出來的**
(見 CPython `typing.py` 的 `_make_nmtuple`,
內部呼叫 `collections.namedtuple(…)`)。
所以 `typing.NamedTuple` 產生的仍是「一個真的 namedtuple 子類別」,
只是額外掛上 `__annotations__` 並提供比較好讀的 class 語法。
<a id=”2″></a>
## 2. `collections.namedtuple`:老牌工廠函式
from collections import namedtuple
# 傳「類別名字串」+「欄位名清單」,回傳一個新的類別
StripRule = namedtuple("StripRule", ["regex", "flag"])
rule = StripRule(regex="(?i)processstep(_\\d+)?$", flag="is_class_name_rm_processstep")
print(rule.regex) # 具名存取:比 rule[0] 好讀
print(rule.flag) # is_class_name_rm_processstep
print(rule[0]) # 仍可用索引 (它是 tuple)特色:
– 回傳值是**不可變** (immutable) 的 tuple 子類別。
– 可用 `rule.regex` 具名存取,也可 `rule[0]` 索引。
– **沒有型別資訊**:`regex` / `flag` 是什麼型別,程式碼上看不出來。
<a id=”3″></a>
## 3. `typing.NamedTuple`:class 語法 + 型別註記
同樣一件事,改用 class 寫法 :
import re
from typing import NamedTuple
class StripRule(NamedTuple):
regex: re.Pattern[str] # 欄位 + 型別
flag: str # 欄位 + 型別
rule = StripRule(re.compile(r"(?i)processstep(_\d+)?$"), "is_class_name_rm_processstep")
print(rule.regex) # re.compile('(?i)processstep(_\\d+)?$', re.IGNORECASE) <- (?i) 會讓 repr 帶出 re.IGNORECASE
print(rule.flag) # is_class_name_rm_processstep好處:
– **每個欄位都標了型別**,
IDE 會自動補全、型別檢查器 (pyright/mypy) 會抓錯。
– 仍然是不可變 tuple:`rule.regex = …` 會報錯,`rule[0]` 也還能用。
– 可加預設值、方法、docstring:
class StripRule(NamedTuple):
"""一條去尾規則:正則 + 對應旗標。"""
regex: re.Pattern[str]
flag: str = "is_class_name_rm_step" # 預設值
def apply(self, name: str) -> str: # 也能加方法
return self.regex.sub("", name)### 3.1 三種寫法對照:同一個 `StripRule`,三種生法
`typing.NamedTuple` 其實也有「函式呼叫」寫法,
但和 `collections` 的差別在於
**typing 版的函式寫法「一定要帶型別」**,
不能只丟名字字串清單:
import re
from collections import namedtuple
from typing import NamedTuple
# (A) collections:欄位只給「名字字串」,不帶型別 —— 合法
StripRule = namedtuple("StripRule", ["regex", "flag"])
# (B) typing 的「函式寫法」:欄位要給 (名字, 型別) 的 tuple —— 合法
StripRule = NamedTuple("StripRule", [("regex", re.Pattern), ("flag", str)])
# (C) typing 的「class 寫法」:本專案採用 —— 合法,最好讀
class StripRule(NamedTuple):
regex: re.Pattern[str]
flag: str**關鍵限制:collections 那種「純名字清單」語法,
搬到 `typing.NamedTuple` 會直接 `ValueError`:**
from typing import NamedTuple
StripRule = NamedTuple("StripRule", ["regex", "flag"])
# ❌ 每個欄位不是 (name, type)
# ValueError: too many values to unpack (expected 2)
# (typing 內部 _make_nmtuple 會對每個欄位做 `for n, t in types` 解包;
# 把字串 "regex" 當可迭代物拆開就爆掉 —— Python 3.11 實測)一句話:**`typing.NamedTuple` 能用函式寫法,
但欄位一定要帶型別;
想「只給名字、不給型別」
就只能回去用 `collections.namedtuple`。**
本專案選 (C),因為 class 寫法最好讀、
又能搭配 `Literal` 限縮 `flag`(見第 5、7 節)。
—
<a id=”4″></a>
## 4. 兩者關係:其實是同一家族
from collections import namedtuple
from typing import NamedTuple
class A(NamedTuple):
x: int
B = namedtuple("B", ["x"])
a = A(1)
print(isinstance(a, tuple)) # True <- NamedTuple 產物也是 tuple
print(a._fields) # ('x',) <- 具備 namedtuple 的 API
print(A.__annotations__) # {'x': <class 'int'>} <- 有型別資訊
print(B.__annotations__) # {} <- collections 版是「空的」(沒有型別資訊,但屬性本身存在)> 註: `collections.namedtuple`
產生的類別**仍有** `__annotations__` 屬性,
只是內容是空 dict `{}`;
> 並非完全沒有這個屬性 (`hasattr(B, “__annotations__”)` 為 `True`)。
差別在「有沒有型別內容」,不是「有沒有這個欄位」。
一句話:**`typing.NamedTuple` =
`collections.namedtuple` + 型別註記 + class 語法糖。**
<a id=”5″></a>
## 5. 本專案為何用 `NamedTuple` 定義 `StripRule`:
讓 `regex` 與 `flag` 一對一綁定,不會錯配
專案裡有三條「去尾規則」,
每條都是「一個 regex 搭配一個旗標名」:
class StripRule(NamedTuple):
regex: re.Pattern[str] # 比對「關鍵字 + 選擇性的 _數字 尾綴」
flag: RmFlagName # 命中此規則時要打的旗標名
PROCESSSTEP_RULE = StripRule(re.compile(r"(?i)processstep(_\d+)?$"), "is_class_name_rm_processstep")
MELTANSTEP_RULE = StripRule(re.compile(r"(?i)meltanstep(_\d+)?$"), "is_class_name_rm_meltanstep")
STEP_RULE = StripRule(re.compile(r"(?i)step(_\d+)?$"), "is_class_name_rm_step")**為什麼要把 regex 和 flag「綁在一起」?**
以前的寫法是分開傳兩個參數:
def build_aliases(tokens, strip_regex, flag_name, ...): # 舊寫法
...
# 呼叫端:regex 與 flag 是「各自獨立」的兩個參數
build_aliases(tokens, re.compile(r"(?i)step(_\d+)?$"), "is_class_name_rm_processstep")
# ^^^^^ step 的 regex ^^^^^ 卻配了 processstep 的 flag → 配錯也沒人擋!型別檢查器只能各自檢查「這是不是 regex」
「這是不是合法 flag 字串」,
**管不了「哪條 regex 該配哪個 flag」**。
用 `NamedTuple` 把兩者釘死成一顆 `rule`,
呼叫端只傳一顆 `PROCESSSTEP_RULE`,
配對在定義時就固定,**從根本杜絕誤配**。
<a id=”6″></a>
## 6. `Literal`:把「合法值」釘死成幾個常數
`Literal` 是「值層級」的型別:它不是說「這是個字串」,
而是說「**只能是這幾個特定字串之一**」。
from typing import Literal
RmFlagName = Literal[
"is_class_name_rm_processstep",
"is_class_name_rm_meltanstep",
"is_class_name_rm_step",
]對比:
def mark(flag: str): ... # 任何字串都算合法,打錯字不會被抓
def mark(flag: RmFlagName): ... # 只有那三個字串合法,打錯字型別檢查器立刻標紅實際差異:
mark("is_class_name_rm_processstep") # OK
mark("is_class_name_rm_stepp") # 多打一個 p:僅「型別檢查器開啟時」才標紅波浪;執行期不報錯,純 str 更完全不抓重點:
– `Literal` **只在型別檢查階段生效**,
執行期 Python **不強制**、
也不影響腳本結果 (照樣跑,不會丟例外)。
– 紅色波浪 **不是一定會出現**——要有型別檢查器在跑才會:
– VS Code + Pylance 預設 `typeCheckingMode = “off”` →
**不標**;設成 `basic`/`strict` 才標。
– 或用 `mypy` / `pyright` 指令檢查 →
會報錯,但只印在終端機,程式本身仍能執行。
– 好處是:**早期防呆** (開了檢查器時打錯字馬上發現) + **自動補全**
(IDE 幫你列出那三個值)。
—
<a id=”7″></a>
## 7. `NamedTuple` + `Literal` 合體:本專案的實戰
把兩者組合起來,就是本專案的核心設計:
import re
from typing import Literal, NamedTuple
# (1) 用 Literal 限縮旗標名的合法值
RmFlagName = Literal[
"is_class_name_rm_processstep",
"is_class_name_rm_meltanstep",
"is_class_name_rm_step",
]
# (2) 用 NamedTuple 把 regex 與 flag 綁成一顆規則,且 flag 的型別就是 RmFlagName
class StripRule(NamedTuple):
regex: re.Pattern[str]
flag: RmFlagName # <- 只接受那三個字串,否則型別檢查器報錯
# (3) 定義三顆規則:配對在此一次釘死,之後不可能拆錯
PROCESSSTEP_RULE = StripRule(re.compile(r"(?i)processstep(_\d+)?$"), "is_class_name_rm_processstep")
MELTANSTEP_RULE = StripRule(re.compile(r"(?i)meltanstep(_\d+)?$"), "is_class_name_rm_meltanstep")
STEP_RULE = StripRule(re.compile(r"(?i)step(_\d+)?$"), "is_class_name_rm_step")如果不小心打錯 flag:
BAD_RULE = StripRule(re.compile(r"(?i)step(_\d+)?$"), "is_class_name_rm_stepX")
# ^^^^^^^^^^^^^^^^^^^^^^^ 型別檢查器 (開啟時) 標紅,執行期不報錯:
# Argument of type "Literal['is_class_name_rm_stepX']" cannot be assigned to
# parameter "flag" of type "RmFlagName"→ **`NamedTuple` 保證「regex 與 flag 不會拆開配錯」,
`Literal` 保證「flag 字串不會打錯」。**
(同第 6 節:上面的紅字只在 Pylance `basic`/`strict` 或
`mypy`/`pyright` 檢查時出現;
實際執行 `BAD_RULE = StripRule(…)` 仍會成功建立,不丟例外。)
—
<a id=”8″></a>
## 8. 常見誤區
1. **大小寫混淆**
– `collections.namedtuple` (小寫,函式)
– `typing.NamedTuple` (大寫,可繼承的類別)
– class 語法 `class StripRule(NamedTuple)` 必須用 `typing` 版。
2. **以為 `Literal` 會在執行期擋值**
– 不會。執行期 `mark(“亂打”)` 照樣跑;`
Literal` 只給型別檢查器/IDE 看。
– 若要執行期強制,得自己 `if flag not in (…)` 或改用 `enum.Enum`。
3. **以為 `NamedTuple` 可以改欄位**
– 不行,它是不可變 tuple。`rule.flag = “x”` 會 `AttributeError`。
– 要「改一份」用 `rule._replace(flag=”…”)`,它回傳**新的**一顆。
4. **想加方法/預設值卻退回 `collections` 版**
– 不必。`typing.NamedTuple`
一樣能加 docstring、預設值、方法 (見第 3 節)。
—
<a id=”9″></a>
## 9. 一頁速查表
# ── collections.namedtuple:快速、無型別 ─────────────────
from collections import namedtuple
StripRule = namedtuple("StripRule", ["regex", "flag"])
# ── typing.NamedTuple:class 語法 + 型別 (本專案採用) ────
from typing import NamedTuple
class StripRule(NamedTuple):
regex: "re.Pattern[str]"
flag: str = "is_class_name_rm_step" # 可給預設值
def apply(self, name): ... # 可加方法
r = StripRule(re.compile(r"(?i)step$"), "is_class_name_rm_step")
r.regex # 具名存取
r[0] # 仍可索引 (是 tuple)
r._fields # ('regex', 'flag')
r._replace(flag="is_class_name_rm_processstep") # 產生新的一顆 (原本不可變)
# ── Literal:限縮合法值 (型別檢查期生效,執行期不強制) ───
from typing import Literal
RmFlagName = Literal["is_class_name_rm_processstep",
"is_class_name_rm_meltanstep",
"is_class_name_rm_step"]**核心心法**
– `NamedTuple`:把「本該綁在一起的欄位」綁成一顆有名字、有型別、不可變的物件。
– `Literal`:把「本該只有幾種合法值的欄位」釘死成常數集合。
– 兩者都以「**讓錯誤在編輯階段就被抓到**」為目標,不影響執行結果。
推薦hahow線上學習python: https://igrape.net/30afN