Python `typing.NamedTuple` (`collections.namedtuple`) 與 `typing.Literal` 教學 — 用型別「防止錯配」; StripRule = NamedTuple(“StripRule”, [(“regex”, re.Pattern), (“flag”, str)]) vs StripRule = namedtuple(“StripRule”, [“regex”, “flag”])

加入好友
加入社群
Python `typing.NamedTuple` (`collections.namedtuple`) 與 `typing.Literal` 教學 — 用型別「防止錯配」; StripRule = NamedTuple("StripRule", [("regex", re.Pattern), ("flag", str)]) vs StripRule = namedtuple("StripRule", ["regex", "flag"]) - 儲蓄保險王

> **主要目的:防止錯配 (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   # 注意:小寫函式

**兩個都存在、都能用,但用途/寫法不同:**

Python `typing.NamedTuple` (`collections.namedtuple`) 與 `typing.Literal` 教學 — 用型別「防止錯配」; StripRule = NamedTuple("StripRule", [("regex", re.Pattern), ("flag", str)]) vs StripRule = namedtuple("StripRule", ["regex", "flag"]) - 儲蓄保險王

關鍵事實:
**`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)
Python `typing.NamedTuple` (`collections.namedtuple`) 與 `typing.Literal` 教學 — 用型別「防止錯配」; StripRule = NamedTuple("StripRule", [("regex", re.Pattern), ("flag", str)]) vs StripRule = namedtuple("StripRule", ["regex", "flag"]) - 儲蓄保險王

特色:

– 回傳值是**不可變** (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
Python `typing.NamedTuple` (`collections.namedtuple`) 與 `typing.Literal` 教學 — 用型別「防止錯配」; StripRule = NamedTuple("StripRule", [("regex", re.Pattern), ("flag", str)]) vs StripRule = namedtuple("StripRule", ["regex", "flag"]) - 儲蓄保險王

好處:

**每個欄位都標了型別**,
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 實測)
Python `typing.NamedTuple` (`collections.namedtuple`) 與 `typing.Literal` 教學 — 用型別「防止錯配」; StripRule = NamedTuple("StripRule", [("regex", re.Pattern), ("flag", str)]) vs StripRule = namedtuple("StripRule", ["regex", "flag"]) - 儲蓄保險王

一句話:**`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")
Python `typing.NamedTuple` (`collections.namedtuple`) 與 `typing.Literal` 教學 — 用型別「防止錯配」; StripRule = NamedTuple("StripRule", [("regex", re.Pattern), ("flag", str)]) vs StripRule = namedtuple("StripRule", ["regex", "flag"]) - 儲蓄保險王

**為什麼要把 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

加入好友
加入社群
Python `typing.NamedTuple` (`collections.namedtuple`) 與 `typing.Literal` 教學 — 用型別「防止錯配」; StripRule = NamedTuple("StripRule", [("regex", re.Pattern), ("flag", str)]) vs StripRule = namedtuple("StripRule", ["regex", "flag"]) - 儲蓄保險王

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡

You may also like...

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *