From ac4ec3636d46aaa5eb1985e83811d4ba270aea9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=9E=E6=98=9F=E5=90=88?= Date: Fri, 24 Apr 2026 03:23:25 +0800 Subject: [PATCH 1/5] 0423-1111405038 --- .../solutions/1111405038/A05-file-tasks.py | 76 ++++++++++++++ .../1111405038/A06-gzip-tempfile-pickle.py | 97 ++++++++++++++++++ .../1111405038/R01-text-io-basics.py | 60 +++++++++++ .../1111405038/R02-path-and-listing.py | 66 +++++++++++++ .../1111405038/U03-bytes-and-encoding.py | 62 ++++++++++++ .../1111405038/U04-stringio-and-lines.py | 90 +++++++++++++++++ .../solutions/1111405038/U_02_itertools.py | 99 +++++++++++++++++++ 7 files changed, 550 insertions(+) create mode 100644 weeks/week-09/solutions/1111405038/A05-file-tasks.py create mode 100644 weeks/week-09/solutions/1111405038/A06-gzip-tempfile-pickle.py create mode 100644 weeks/week-09/solutions/1111405038/R01-text-io-basics.py create mode 100644 weeks/week-09/solutions/1111405038/R02-path-and-listing.py create mode 100644 weeks/week-09/solutions/1111405038/U03-bytes-and-encoding.py create mode 100644 weeks/week-09/solutions/1111405038/U04-stringio-and-lines.py create mode 100644 weeks/week-09/solutions/1111405038/U_02_itertools.py diff --git a/weeks/week-09/solutions/1111405038/A05-file-tasks.py b/weeks/week-09/solutions/1111405038/A05-file-tasks.py new file mode 100644 index 000000000..f353b8747 --- /dev/null +++ b/weeks/week-09/solutions/1111405038/A05-file-tasks.py @@ -0,0 +1,76 @@ +# A05. 綜合應用:僅寫新檔 + 目錄統計(5.5 / 5.13 / 5.1) +# Bloom: Apply — 把前面學到的 API 組起來解小任務 + +# Path:跨平台路徑操作(組路徑、檢查存在、遞迴找檔) +from pathlib import Path +# date:取得今天日期,用來生成「每日唯一」的日記檔名 +from datetime import date + +# ── 任務一:日記小工具(5.5 的 'x' 模式) ────────────── +# 規則:每天只能建一次;同一天重複執行要提示「已存在」。 +# isoformat() 會得到 YYYY-MM-DD 格式,適合放進檔名 +today = date.today().isoformat() # 例如 2026-04-23 + +# 每天一個檔案,例如 diary-2026-04-23.txt +diary = Path(f"diary-{today}.txt") + +try: + # 'x' = exclusive create:檔案不存在才建立,已存在就丟 FileExistsError + # 這個模式很適合「避免覆蓋」的情境(像每日日記、初始化設定檔) + with open(diary, "x", encoding="utf-8") as f: # 'x' = exclusive create + f.write(f"# {today} 日記\n") + f.write("今天學了檔案 I/O。\n") + + # 只有成功建立新檔時才會走到這裡 + print(f"已建立 {diary}") +except FileExistsError: + # 若同日已建立過,就提示並保留既有內容 + print(f"{diary} 今天已寫過,保留原內容不覆蓋") + +# ── 任務二:統計某資料夾裡 .py 檔的行數 ──────────────── +# 走訪目錄 → 逐檔逐行讀 → 累計三個數字 +def count_py(folder: Path): + # total:總行數(含空行) + # nonblank:非空白行數 + # defs:去掉前後空白後,以 "def " 開頭的行數 + total, nonblank, defs = 0, 0, 0 + + # rglob("*.py"):遞迴掃描 folder 及所有子資料夾中的 .py 檔 + for p in folder.rglob("*.py"): + # errors="replace":遇到少數無法解碼字元時用替代字元,不讓程式中斷 + with open(p, "rt", encoding="utf-8", errors="replace") as f: + for line in f: + # 每讀到一行就累計總行數 + total += 1 + + # strip() 後便於判斷空行與比對開頭關鍵字 + s = line.strip() + + # 不是空字串就算非空白行 + if s: + nonblank += 1 + + # 函式定義通常以 def 開頭(此處是簡化統計,非語法解析) + if s.startswith("def "): + defs += 1 + + # 回傳三個統計值,供呼叫端顯示或後續處理 + return total, nonblank, defs + +# 示範目標資料夾:從目前目錄往上兩層,再進入 week-04/in-class +target = Path("..") / ".." / "week-04" / "in-class" + +# 先檢查目錄是否存在,避免直接掃描不存在路徑造成問題 +if target.exists(): + total, nonblank, defs = count_py(target) + print(f"{target}") + print(f" 總行數 : {total}") + print(f" 非空白行 : {nonblank}") + print(f" def 起頭行數 : {defs}") +else: + print(f"示範目錄不存在:{target}") + +# ── 課堂延伸挑戰(自行嘗試) ─────────────────────────── +# 1) 把日記工具改成「附加」模式 'a':同一天可多次追寫一行時間戳。 +# 2) count_py 再多算一個「註解行(以 # 開頭)」的數字。 +# 3) 把統計結果用 print(..., sep='\t', file=f) 寫到 stats.tsv。 diff --git a/weeks/week-09/solutions/1111405038/A06-gzip-tempfile-pickle.py b/weeks/week-09/solutions/1111405038/A06-gzip-tempfile-pickle.py new file mode 100644 index 000000000..daeceae03 --- /dev/null +++ b/weeks/week-09/solutions/1111405038/A06-gzip-tempfile-pickle.py @@ -0,0 +1,97 @@ +# A06. 壓縮檔、臨時資料夾、物件序列化(5.7 / 5.19 / 5.21) +# Bloom: Apply — 能把標準庫工具組合起來解一個小任務 + +# gzip:讀寫 .gz 壓縮檔(介面與 open 類似) +import gzip +# pickle:將 Python 物件序列化成 bytes(僅建議在 Python 生態內使用) +import pickle +# tempfile:建立會自動清理的暫存檔案/資料夾 +import tempfile +# Path:路徑與檔案操作的物件化 API +from pathlib import Path + +# ── 5.7 讀寫壓縮檔:gzip.open 幾乎和 open 一樣 ───────── +# 寫 .gz(文字模式要記得 encoding) +# 文字模式 wt:寫入 str,gzip 會先編碼再壓縮 +with gzip.open("notes.txt.gz", "wt", encoding="utf-8") as f: + f.write("第一行筆記\n") + f.write("第二行筆記\n") + +# 讀回:直接逐行迭代 +# 文字模式 rt:讀出時會先解壓縮,再依 encoding 解碼成 str +with gzip.open("notes.txt.gz", "rt", encoding="utf-8") as f: + for line in f: + # line 通常含尾端換行,顯示前用 rstrip() 去掉 + print("gz:", line.rstrip()) + +# 也能用 'wb'/'rb' 處理二進位資料 +# 二進位模式 wb:直接寫 bytes,不處理文字編碼 +with gzip.open("blob.bin.gz", "wb") as f: + f.write(b"\x00\x01\x02\x03") + +# stat().st_size 是「壓縮後檔案」實際大小(單位 bytes) +print("blob size:", Path("blob.bin.gz").stat().st_size, "bytes") + +# ── 5.19 臨時檔案與資料夾:離開 with 自動清理 ────────── +# 場景:想跑個小實驗但不想在專案亂留檔 +# TemporaryDirectory() 會建立一個暫存目錄,離開 with 後自動刪除 +with tempfile.TemporaryDirectory() as tmp: + # tmp 原本是字串路徑,轉成 Path 方便後續操作 + tmp = Path(tmp) + print("暫存資料夾:", tmp) + + # 在裡面寫幾個檔 + (tmp / "a.txt").write_text("hello\n", encoding="utf-8") + (tmp / "b.txt").write_text("world\n", encoding="utf-8") + + # 列出內容 + # iterdir() 只列當層,不遞迴 + for p in tmp.iterdir(): + print(" ", p.name, "→", p.read_text(encoding="utf-8").rstrip()) + +# 離開 with 後,tmp 已自動刪除 +print("離開後還存在嗎?", tmp.exists()) # False + +# 單一臨時檔:NamedTemporaryFile +# delete=False:關閉檔案後不立刻刪除,方便跨流程/外部程式再使用 +with tempfile.NamedTemporaryFile("wt", delete=False, suffix=".log", + encoding="utf-8") as f: + f.write("暫存 log\n") + # f.name 是暫存檔實際路徑 + log_path = f.name +print("暫存檔位置:", log_path) + +# 用完後手動刪除,避免留下垃圾檔 +Path(log_path).unlink() # 用完自己刪 + +# ── 5.21 pickle:把 Python 物件「原樣」存檔 ──────────── +# 適用:dict/list/自訂類別;不適用:跨語言、長期存檔(用 json 更穩) +scores = { + "alice": [90, 85, 92], + "bob": [70, 75, 80], + "carol": [88, 91, 95], +} + +# 注意:pickle 是 bytes → 一定要 'wb'/'rb' +# dump:把 Python 物件序列化並寫入檔案 +with open("scores.pkl", "wb") as f: + pickle.dump(scores, f) + +# load:從檔案讀回 bytes 並反序列化為 Python 物件 +with open("scores.pkl", "rb") as f: + loaded = pickle.load(f) + +print("讀回的物件:", loaded) +print("型別一致?", type(loaded) is dict) # True +print("內容相等?", loaded == scores) # True + +# 讀回資料後可直接做一般運算 +print("alice 平均:", sum(loaded["alice"]) / 3) # 89.0 + +# ⚠️ 安全提醒:pickle.load 會執行內嵌指令, +# 絕對不要對「來路不明」的 .pkl 檔做 load。 + +# ── 課堂延伸挑戰 ─────────────────────────────────────── +# 1) 把 scores 存成 gzip 壓縮後的 pickle:gzip.open('scores.pkl.gz','wb') +# 2) 用 TemporaryDirectory 跑完整流程(寫→讀→比對),不在專案留任何檔 +# 3) 試著 pickle 一個 lambda,觀察錯誤訊息(pickle 不能存 lambda) diff --git a/weeks/week-09/solutions/1111405038/R01-text-io-basics.py b/weeks/week-09/solutions/1111405038/R01-text-io-basics.py new file mode 100644 index 000000000..56aefc493 --- /dev/null +++ b/weeks/week-09/solutions/1111405038/R01-text-io-basics.py @@ -0,0 +1,60 @@ +# R01. 文本 I/O 基本式(5.1 / 5.2 / 5.3 / 5.17) +# Bloom: Remember — 會叫出 open/print 的基本參數 + +# Path 提供方便的檔案路徑操作與快速讀寫方法(read_text / write_text) +from pathlib import Path + +# ── 5.1 讀寫文本檔 ───────────────────────────────────── +# 寫入:mode='wt'(write text) +# 重點:文字檔應明確指定 encoding='utf-8',避免不同系統預設編碼造成亂碼 +path = Path("hello.txt") +with open(path, "wt", encoding="utf-8") as f: + # write() 會回傳寫入字元數;這裡不需使用回傳值 + f.write("你好,Python\n") + f.write("第二行\n") + +# 讀回:一次讀完 vs 逐行讀 +with open(path, "rt", encoding="utf-8") as f: + # f.read() 會一次把整個檔案載入記憶體 + # 適合小檔;大檔可能吃掉大量 RAM + print(f.read()) # 一次讀完(小檔才適合) + +with open(path, "rt", encoding="utf-8") as f: + # 逐行迭代:一次只讀一行,對大檔更安全、穩定 + for line in f: # 大檔必備:逐行迭代 + # line 本身通常含有尾端換行,因此用 rstrip() 去除右側空白/換行 + print(line.rstrip()) + +# ── 5.2 print 導向檔案 ───────────────────────────────── +# print(..., file=f) 可把輸出改寫到檔案,而不是終端機 +with open("log.txt", "wt", encoding="utf-8") as f: + print("登入成功", file=f) + print("使用者:", "alice", file=f) + +# ── 5.3 調整分隔符與行終止符 ─────────────────────────── +fruits = ["apple", "banana", "cherry"] +with open("fruits.csv", "wt", encoding="utf-8") as f: + # *fruits 展開清單,sep="," 指定欄位分隔 + # end="\n" 保留每次 print 結尾換行(預設本來就是 \n) + print(*fruits, sep=",", end="\n", file=f) + +# end='' 可避免多一個換行 +with open("fruits.csv", "at", encoding="utf-8") as f: + # 'at' = append text:在原檔尾端追加,不覆蓋既有內容 + print("date", end=",", file=f) + print("2026-04-23", file=f) + +# 用 Path.read_text 快速讀回整個文字檔並印出 +print(Path("fruits.csv").read_text(encoding="utf-8")) +# apple,banana,cherry +# date,2026-04-23 + +# ── 5.17 文字模式 vs 位元組模式提醒 ──────────────────── +# 'wt' 寫 str、'wb' 寫 bytes;寫錯型別會 TypeError +try: + with open("bad.txt", "wt", encoding="utf-8") as f: + # 文字模式要求 str;這裡故意傳 bytes 來示範錯誤 + f.write(b"bytes in text mode") # ← 會錯 +except TypeError as e: + # 捕捉後印出錯誤,讓教學流程能繼續執行 + print("錯誤示範:", e) diff --git a/weeks/week-09/solutions/1111405038/R02-path-and-listing.py b/weeks/week-09/solutions/1111405038/R02-path-and-listing.py new file mode 100644 index 000000000..2ac86f378 --- /dev/null +++ b/weeks/week-09/solutions/1111405038/R02-path-and-listing.py @@ -0,0 +1,66 @@ +# R02. 路徑操作與目錄列舉(5.11 / 5.12 / 5.13) +# Bloom: Remember — 會用 pathlib 組路徑、檢查存在、列出檔案 + +# os:傳統路徑/檔案系統 API,像 os.path.join、os.listdir +import os +# Path:pathlib 的核心類別,物件化路徑操作更直覺、可讀性更高 +from pathlib import Path + +# ── 5.11 組路徑:pathlib 是現代寫法 ──────────────────── +# 用 / 來串接路徑(不是做除法),會自動處理不同平台分隔符 +base = Path("weeks") / "week-09" + +# Path 物件常用屬性: +# - name:最後一段名稱 +# - parent:上一層路徑 +# - suffix:副檔名(含點),資料夾通常為空字串 +print(base) # weeks/week-09(Windows 會自動變成反斜線) +print(base.name) # week-09 +print(base.parent) # weeks +print(base.suffix) # ''(無副檔名) + +# 檔名相關拆解: +# - stem:去掉副檔名後的主檔名 +# - suffix:副檔名 +f = Path("hello.txt") +print(f.stem, f.suffix) # hello .txt + +# 相容舊寫法:os.path.join +# 舊專案常見,了解有助於閱讀既有程式碼 +print(os.path.join("weeks", "week-09", "README.md")) + +# ── 5.12 存在判斷 ────────────────────────────────────── +p = Path("hello.txt") + +# 三個常用檢查: +# - exists():路徑是否存在(檔案或資料夾都算) +# - is_file():是否是檔案 +# - is_dir():是否是資料夾 +print(p.exists()) # 是否存在 +print(p.is_file()) # 是否是檔案 +print(p.is_dir()) # 是否是資料夾 + +# 常見防呆:先檢查存在,再決定是否讀取/處理 +missing = Path("no_such_file.txt") +if not missing.exists(): + print(f"{missing} 不存在,略過讀取") + +# ── 5.13 列出資料夾內容 ──────────────────────────────── +# 目前工作目錄(current working directory) +here = Path(".") + +# 只列當層 +# os.listdir 回傳字串名稱,不含完整路徑 +for name in os.listdir(here): + print("listdir:", name) + +# 只抓 .py(當層) +# Path.glob 回傳 Path 物件,可直接做後續檔案操作 +for p in here.glob("*.py"): + print("glob:", p) + +# 遞迴抓所有 .py(含子資料夾) +# rglob 會深入子目錄;在大型專案中結果可能很多 +for p in Path("..").rglob("*.py"): + print("rglob:", p) + break # 示範用,只印第一個 diff --git a/weeks/week-09/solutions/1111405038/U03-bytes-and-encoding.py b/weeks/week-09/solutions/1111405038/U03-bytes-and-encoding.py new file mode 100644 index 000000000..0c9c4d5f2 --- /dev/null +++ b/weeks/week-09/solutions/1111405038/U03-bytes-and-encoding.py @@ -0,0 +1,62 @@ +# U03. 文字 vs 位元組、編碼觀念(5.1 encoding / 5.4) +# Bloom: Understand — 能解釋什麼時候用 'rb'、為什麼要指定 encoding + +# Path 提供直觀的路徑與檔案操作 API(write_text / write_bytes / read_text) +from pathlib import Path + +# ── 5.4 二進位讀寫:圖片、zip、任何非文字 ─────────────── +# 先造一個「假 PNG」:只寫前 8 bytes 的 magic number(檔案簽章) +# PNG 固定檔頭為:89 50 4E 47 0D 0A 1A 0A(16 進位) +# bytes([...]) 會把 0~255 的整數序列轉成位元組物件 +magic = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) + +# write_bytes:以二進位方式寫入,不涉及文字編碼 +Path("fake.png").write_bytes(magic) + +# 讀回前 8 bytes,對照 PNG 檔頭 +# 注意這裡用 'rb'(read binary):讀到的是 bytes,不是 str +with open("fake.png", "rb") as f: + head = f.read(8) + +# bytes 的印出格式會帶 b'' 前綴 +print(head) # b'\x89PNG\r\n\x1a\n' + +# 直接比較兩個 bytes 內容是否完全一致 +print(head == magic) # True + +# bytes 可逐位元組迭代(拿到 int,不是 str) +# head[:4] 只取前四個位元組,示範每個位元組可轉成十六進位顯示 +for b in head[:4]: + print(b, hex(b)) + +# ── 文字 vs 位元組的型別差 ───────────────────────────── +s = "你好" + +# encode:把 Python 字串(Unicode)依指定編碼轉成 bytes +b = s.encode("utf-8") # str → bytes + +# 型別對照:文字是 str、編碼後是 bytes +print(s, type(s)) # +print(b, type(b)) # + +# decode:把 bytes 按相同編碼還原回字串 +print(b.decode("utf-8")) # bytes → str + +# ── 5.1 encoding 參數:寫錯會爛掉 ────────────────────── +# 用 UTF-8 寫入中文文字檔 +Path("zh.txt").write_text("中文測試\n", encoding="utf-8") + +# 正常:用 utf-8 讀 utf-8 寫的檔 +print(Path("zh.txt").read_text(encoding="utf-8")) + +# 故意弄錯:用 big5 解 utf-8 → UnicodeDecodeError +# 不同編碼規則彼此不一定相容,讀取時編碼要和寫入時一致 +try: + print(Path("zh.txt").read_text(encoding="big5")) +except UnicodeDecodeError as e: + # 捕捉解碼失敗,避免程式直接中止 + print("解碼錯誤:", e) + +# 小結: +# - 文字檔 → 'rt'/'wt',一律明示 encoding='utf-8' +# - 非文字(png/zip/pickle)→ 'rb'/'wb',不談 encoding diff --git a/weeks/week-09/solutions/1111405038/U04-stringio-and-lines.py b/weeks/week-09/solutions/1111405038/U04-stringio-and-lines.py new file mode 100644 index 000000000..e112bef46 --- /dev/null +++ b/weeks/week-09/solutions/1111405038/U04-stringio-and-lines.py @@ -0,0 +1,90 @@ +# U04. 類檔案物件 StringIO 與逐行處理(5.6 / 5.1 逐行) +# Bloom: Understand — 知道 file-like 是鴨子型別,能把記憶體當檔案用 + +# io:提供 StringIO/BytesIO,讓「記憶體中的資料」看起來像檔案物件 +import io +# Path:以物件方式操作路徑,跨平台且比字串拼接更安全 +from pathlib import Path + +# ── 5.6 StringIO:記憶體裡的「假檔案」 ───────────────── +# 建立一個空的文字緩衝區(in-memory text stream) +# 它支援 .write()、.read()、迭代等檔案常見操作 +buf = io.StringIO() + +# print(..., file=buf) 代表把輸出導向 buf,而不是終端機 +# 這和 print(..., file=open(...)) 的概念完全一致 +print("第一行", file=buf) +print("第二行", file=buf) +print("第三行", file=buf) + +# getvalue() 會一次取出目前緩衝區中的完整文字內容 +# 常用於測試:先把函式輸出寫到 StringIO,再比對字串是否正確 +text = buf.getvalue() +print("---StringIO 內容---") +print(text) + +# 寫入後,游標會停在尾端;若要「從頭讀」,要先 seek(0) +# 這和實體檔案的讀寫游標行為相同 +buf.seek(0) + +# enumerate(buf, 1):逐行迭代並從 1 開始編號 +# line 會保留行尾 '\n',所以輸出前常用 rstrip() 去掉尾端換行 +for i, line in enumerate(buf, 1): + print(i, line.rstrip()) + +# 為什麼有用?任何收 file-like 的 API(csv、json、logging) +# 都能塞 StringIO,不必真的寫到磁碟、方便測試。 +import csv + +# 第二個記憶體檔案:示範 csv.writer 也能直接寫入 StringIO +mem = io.StringIO() +writer = csv.writer(mem) + +# writerow 會自動依 CSV 規則處理欄位分隔與必要的跳脫 +writer.writerow(["name", "score"]) +writer.writerow(["alice", 90]) + +# 取出記憶體中的 CSV 文字,可直接拿去顯示、傳輸或測試比對 +print("---CSV in memory---") +print(mem.getvalue()) + +# ── 5.1 延伸:逐行處理檔案(大檔友善) ───────────────── +# 先造一個多行檔:中間刻意放空行,方便示範「過濾空白行」 +src = Path("poem.txt") + +# write_text 是 Path 的便捷 API,等價於 open(...).write(...) +# 明確指定 encoding='utf-8' 可避免跨系統編碼差異 +src.write_text("床前明月光\n\n疑是地上霜\n\n舉頭望明月\n低頭思故鄉\n", encoding="utf-8") + +# 任務:過濾空行、加上行號、寫到新檔 +dst = Path("poem_numbered.txt") + +# 同時開啟來源與目的檔案: +# - src 用 'rt' 文字讀取 +# - dst 用 'wt' 文字寫入(若已存在會覆蓋) +# 反斜線(\)是換行續寫語法,讓 with 區塊更易讀 +with open(src, "rt", encoding="utf-8") as fin, \ + open(dst, "wt", encoding="utf-8") as fout: + # n:輸出的「有效行」計數器(不含空白行) + n = 0 + + # 逐行處理是大檔實務中的基本技巧: + # 一次只保留當前行,記憶體使用量穩定 + for line in fin: + # rstrip() 去除右側空白與換行,便於判斷是否為空行 + line = line.rstrip() + + # 空字串代表空行(或只有空白),直接跳過不輸出 + if not line: + continue + + # 只有非空行才編號,讓結果連續 + n += 1 + + # {n:02d}:數字補 0 至兩位,例如 01、02、03 + # file=fout:把格式化後字串寫到目的檔 + print(f"{n:02d}. {line}", file=fout) + +# 最後讀回輸出檔,確認結果是否符合預期 +print("---加行號後---") +print(dst.read_text(encoding="utf-8")) diff --git a/weeks/week-09/solutions/1111405038/U_02_itertools.py b/weeks/week-09/solutions/1111405038/U_02_itertools.py new file mode 100644 index 000000000..d42b94d54 --- /dev/null +++ b/weeks/week-09/solutions/1111405038/U_02_itertools.py @@ -0,0 +1,99 @@ +# Understand(理解)- itertools 工具函數 + +# 從 itertools 匯入常見工具: +# - islice:對「可迭代物件」做切片,不必先轉 list +# - dropwhile:條件成立時持續丟棄,直到第一次不成立後全部保留 +# - takewhile:條件成立時持續取用,遇到第一次不成立就停止 +# - chain:把多個可迭代物件串成一個連續序列 +# - permutations:排列(順序不同算不同) +# - combinations:組合(順序不同視為相同) +from itertools import islice, dropwhile, takewhile, chain, permutations, combinations + +print("--- islice() 切片 ---") + + +# 產生一個無限遞增的產生器(generator) +# 與 range 不同:它不會一次建立整個序列,適合示範惰性計算 +def count(n): + i = n + while True: + yield i + i += 1 + + +# 從 0 開始的無限序列:0,1,2,3,4,... +c = count(0) + +# islice(c, 5, 10) 類似序列切片 [5:10] +# 會取到第 5~9 個元素(不含 10)=> [5,6,7,8,9] +result = list(islice(c, 5, 10)) +print(f"islice(c, 5, 10): {result}") + +print("\n--- dropwhile() 條件跳過 ---") +nums = [1, 3, 5, 2, 4, 6] + +# dropwhile(lambda x: x < 5, nums) +# 流程: +# 1) 一開始只要 x < 5 就丟掉(1、3 被丟掉) +# 2) 遇到 5 時條件不成立,從這一刻起「後面全部保留」 +# 3) 所以結果是 [5,2,4,6](後面的 2、4 不會再被判斷丟棄) +result = list(dropwhile(lambda x: x < 5, nums)) +print(f"dropwhile(x<5, {nums}): {result}") + +print("\n--- takewhile() 條件取用 ---") + +# takewhile(lambda x: x < 5, nums) +# 從頭開始「只要條件成立就取」,第一個不成立就立即停止 +# 因為遇到 5 就停止,所以只會得到 [1,3] +result = list(takewhile(lambda x: x < 5, nums)) +print(f"takewhile(x<5, {nums}): {result}") + +print("\n--- chain() 串聯 ---") +a = [1, 2] +b = [3, 4] +c = [5] + +# chain(a, b, c) 不會建立中間大陣列,而是逐段迭代輸出 +# 最後轉成 list 才真正得到 [1,2,3,4,5] +print(f"chain(a, b, c): {list(chain(a, b, c))}") + +print("\n--- permutations() 排列 ---") +items = ["a", "b", "c"] +print(f"permutations(items):") + +# permutations(items) 預設 r=len(items) +# 會列出 3 個元素的所有排列,共 3! = 6 種 +for p in permutations(items): + print(f" {p}") + +print(f"permutations(items, 2):") + +# permutations(items, 2):從 3 個元素中挑 2 個並考慮順序 +# 數量為 P(3,2)=3*2=6 +for p in permutations(items, 2): + print(f" {p}") + +print("\n--- combinations() 組合 ---") +print(f"combinations(items, 2):") + +# combinations(items, 2):從 3 個元素中挑 2 個,不考慮順序 +# ('a','b') 與 ('b','a') 視為同一組,所以只有 C(3,2)=3 種 +for c in combinations(items, 2): + print(f" {c}") + +print("\n--- 組合應用:密碼窮舉 ---") +chars = ["A", "B", "1"] +print("2位數密碼:") + +# 用 permutations(chars, 2) 產生「不重複字元」的 2 位密碼 +for p in permutations(chars, 2): + print(f" {''.join(p)}") + +print("2位數密碼(可重複):") + +# combinations_with_replacement:允許重複取元素,但不考慮順序 +# 例如會有 AA、AB、A1、BB、B1、11,但不會另外出現 BA、1A... +from itertools import combinations_with_replacement + +for p in combinations_with_replacement(chars, 2): + print(f" {''.join(p)}") From 502b9fe0bd4034bf867a7705ac9b9420f766c3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=9E=E6=98=9F=E5=90=88?= Date: Fri, 24 Apr 2026 03:47:32 +0800 Subject: [PATCH 2/5] Revert "0423-1111405038" This reverts commit ac4ec3636d46aaa5eb1985e83811d4ba270aea9b. --- .../solutions/1111405038/A05-file-tasks.py | 76 -------------- .../1111405038/A06-gzip-tempfile-pickle.py | 97 ------------------ .../1111405038/R01-text-io-basics.py | 60 ----------- .../1111405038/R02-path-and-listing.py | 66 ------------- .../1111405038/U03-bytes-and-encoding.py | 62 ------------ .../1111405038/U04-stringio-and-lines.py | 90 ----------------- .../solutions/1111405038/U_02_itertools.py | 99 ------------------- 7 files changed, 550 deletions(-) delete mode 100644 weeks/week-09/solutions/1111405038/A05-file-tasks.py delete mode 100644 weeks/week-09/solutions/1111405038/A06-gzip-tempfile-pickle.py delete mode 100644 weeks/week-09/solutions/1111405038/R01-text-io-basics.py delete mode 100644 weeks/week-09/solutions/1111405038/R02-path-and-listing.py delete mode 100644 weeks/week-09/solutions/1111405038/U03-bytes-and-encoding.py delete mode 100644 weeks/week-09/solutions/1111405038/U04-stringio-and-lines.py delete mode 100644 weeks/week-09/solutions/1111405038/U_02_itertools.py diff --git a/weeks/week-09/solutions/1111405038/A05-file-tasks.py b/weeks/week-09/solutions/1111405038/A05-file-tasks.py deleted file mode 100644 index f353b8747..000000000 --- a/weeks/week-09/solutions/1111405038/A05-file-tasks.py +++ /dev/null @@ -1,76 +0,0 @@ -# A05. 綜合應用:僅寫新檔 + 目錄統計(5.5 / 5.13 / 5.1) -# Bloom: Apply — 把前面學到的 API 組起來解小任務 - -# Path:跨平台路徑操作(組路徑、檢查存在、遞迴找檔) -from pathlib import Path -# date:取得今天日期,用來生成「每日唯一」的日記檔名 -from datetime import date - -# ── 任務一:日記小工具(5.5 的 'x' 模式) ────────────── -# 規則:每天只能建一次;同一天重複執行要提示「已存在」。 -# isoformat() 會得到 YYYY-MM-DD 格式,適合放進檔名 -today = date.today().isoformat() # 例如 2026-04-23 - -# 每天一個檔案,例如 diary-2026-04-23.txt -diary = Path(f"diary-{today}.txt") - -try: - # 'x' = exclusive create:檔案不存在才建立,已存在就丟 FileExistsError - # 這個模式很適合「避免覆蓋」的情境(像每日日記、初始化設定檔) - with open(diary, "x", encoding="utf-8") as f: # 'x' = exclusive create - f.write(f"# {today} 日記\n") - f.write("今天學了檔案 I/O。\n") - - # 只有成功建立新檔時才會走到這裡 - print(f"已建立 {diary}") -except FileExistsError: - # 若同日已建立過,就提示並保留既有內容 - print(f"{diary} 今天已寫過,保留原內容不覆蓋") - -# ── 任務二:統計某資料夾裡 .py 檔的行數 ──────────────── -# 走訪目錄 → 逐檔逐行讀 → 累計三個數字 -def count_py(folder: Path): - # total:總行數(含空行) - # nonblank:非空白行數 - # defs:去掉前後空白後,以 "def " 開頭的行數 - total, nonblank, defs = 0, 0, 0 - - # rglob("*.py"):遞迴掃描 folder 及所有子資料夾中的 .py 檔 - for p in folder.rglob("*.py"): - # errors="replace":遇到少數無法解碼字元時用替代字元,不讓程式中斷 - with open(p, "rt", encoding="utf-8", errors="replace") as f: - for line in f: - # 每讀到一行就累計總行數 - total += 1 - - # strip() 後便於判斷空行與比對開頭關鍵字 - s = line.strip() - - # 不是空字串就算非空白行 - if s: - nonblank += 1 - - # 函式定義通常以 def 開頭(此處是簡化統計,非語法解析) - if s.startswith("def "): - defs += 1 - - # 回傳三個統計值,供呼叫端顯示或後續處理 - return total, nonblank, defs - -# 示範目標資料夾:從目前目錄往上兩層,再進入 week-04/in-class -target = Path("..") / ".." / "week-04" / "in-class" - -# 先檢查目錄是否存在,避免直接掃描不存在路徑造成問題 -if target.exists(): - total, nonblank, defs = count_py(target) - print(f"{target}") - print(f" 總行數 : {total}") - print(f" 非空白行 : {nonblank}") - print(f" def 起頭行數 : {defs}") -else: - print(f"示範目錄不存在:{target}") - -# ── 課堂延伸挑戰(自行嘗試) ─────────────────────────── -# 1) 把日記工具改成「附加」模式 'a':同一天可多次追寫一行時間戳。 -# 2) count_py 再多算一個「註解行(以 # 開頭)」的數字。 -# 3) 把統計結果用 print(..., sep='\t', file=f) 寫到 stats.tsv。 diff --git a/weeks/week-09/solutions/1111405038/A06-gzip-tempfile-pickle.py b/weeks/week-09/solutions/1111405038/A06-gzip-tempfile-pickle.py deleted file mode 100644 index daeceae03..000000000 --- a/weeks/week-09/solutions/1111405038/A06-gzip-tempfile-pickle.py +++ /dev/null @@ -1,97 +0,0 @@ -# A06. 壓縮檔、臨時資料夾、物件序列化(5.7 / 5.19 / 5.21) -# Bloom: Apply — 能把標準庫工具組合起來解一個小任務 - -# gzip:讀寫 .gz 壓縮檔(介面與 open 類似) -import gzip -# pickle:將 Python 物件序列化成 bytes(僅建議在 Python 生態內使用) -import pickle -# tempfile:建立會自動清理的暫存檔案/資料夾 -import tempfile -# Path:路徑與檔案操作的物件化 API -from pathlib import Path - -# ── 5.7 讀寫壓縮檔:gzip.open 幾乎和 open 一樣 ───────── -# 寫 .gz(文字模式要記得 encoding) -# 文字模式 wt:寫入 str,gzip 會先編碼再壓縮 -with gzip.open("notes.txt.gz", "wt", encoding="utf-8") as f: - f.write("第一行筆記\n") - f.write("第二行筆記\n") - -# 讀回:直接逐行迭代 -# 文字模式 rt:讀出時會先解壓縮,再依 encoding 解碼成 str -with gzip.open("notes.txt.gz", "rt", encoding="utf-8") as f: - for line in f: - # line 通常含尾端換行,顯示前用 rstrip() 去掉 - print("gz:", line.rstrip()) - -# 也能用 'wb'/'rb' 處理二進位資料 -# 二進位模式 wb:直接寫 bytes,不處理文字編碼 -with gzip.open("blob.bin.gz", "wb") as f: - f.write(b"\x00\x01\x02\x03") - -# stat().st_size 是「壓縮後檔案」實際大小(單位 bytes) -print("blob size:", Path("blob.bin.gz").stat().st_size, "bytes") - -# ── 5.19 臨時檔案與資料夾:離開 with 自動清理 ────────── -# 場景:想跑個小實驗但不想在專案亂留檔 -# TemporaryDirectory() 會建立一個暫存目錄,離開 with 後自動刪除 -with tempfile.TemporaryDirectory() as tmp: - # tmp 原本是字串路徑,轉成 Path 方便後續操作 - tmp = Path(tmp) - print("暫存資料夾:", tmp) - - # 在裡面寫幾個檔 - (tmp / "a.txt").write_text("hello\n", encoding="utf-8") - (tmp / "b.txt").write_text("world\n", encoding="utf-8") - - # 列出內容 - # iterdir() 只列當層,不遞迴 - for p in tmp.iterdir(): - print(" ", p.name, "→", p.read_text(encoding="utf-8").rstrip()) - -# 離開 with 後,tmp 已自動刪除 -print("離開後還存在嗎?", tmp.exists()) # False - -# 單一臨時檔:NamedTemporaryFile -# delete=False:關閉檔案後不立刻刪除,方便跨流程/外部程式再使用 -with tempfile.NamedTemporaryFile("wt", delete=False, suffix=".log", - encoding="utf-8") as f: - f.write("暫存 log\n") - # f.name 是暫存檔實際路徑 - log_path = f.name -print("暫存檔位置:", log_path) - -# 用完後手動刪除,避免留下垃圾檔 -Path(log_path).unlink() # 用完自己刪 - -# ── 5.21 pickle:把 Python 物件「原樣」存檔 ──────────── -# 適用:dict/list/自訂類別;不適用:跨語言、長期存檔(用 json 更穩) -scores = { - "alice": [90, 85, 92], - "bob": [70, 75, 80], - "carol": [88, 91, 95], -} - -# 注意:pickle 是 bytes → 一定要 'wb'/'rb' -# dump:把 Python 物件序列化並寫入檔案 -with open("scores.pkl", "wb") as f: - pickle.dump(scores, f) - -# load:從檔案讀回 bytes 並反序列化為 Python 物件 -with open("scores.pkl", "rb") as f: - loaded = pickle.load(f) - -print("讀回的物件:", loaded) -print("型別一致?", type(loaded) is dict) # True -print("內容相等?", loaded == scores) # True - -# 讀回資料後可直接做一般運算 -print("alice 平均:", sum(loaded["alice"]) / 3) # 89.0 - -# ⚠️ 安全提醒:pickle.load 會執行內嵌指令, -# 絕對不要對「來路不明」的 .pkl 檔做 load。 - -# ── 課堂延伸挑戰 ─────────────────────────────────────── -# 1) 把 scores 存成 gzip 壓縮後的 pickle:gzip.open('scores.pkl.gz','wb') -# 2) 用 TemporaryDirectory 跑完整流程(寫→讀→比對),不在專案留任何檔 -# 3) 試著 pickle 一個 lambda,觀察錯誤訊息(pickle 不能存 lambda) diff --git a/weeks/week-09/solutions/1111405038/R01-text-io-basics.py b/weeks/week-09/solutions/1111405038/R01-text-io-basics.py deleted file mode 100644 index 56aefc493..000000000 --- a/weeks/week-09/solutions/1111405038/R01-text-io-basics.py +++ /dev/null @@ -1,60 +0,0 @@ -# R01. 文本 I/O 基本式(5.1 / 5.2 / 5.3 / 5.17) -# Bloom: Remember — 會叫出 open/print 的基本參數 - -# Path 提供方便的檔案路徑操作與快速讀寫方法(read_text / write_text) -from pathlib import Path - -# ── 5.1 讀寫文本檔 ───────────────────────────────────── -# 寫入:mode='wt'(write text) -# 重點:文字檔應明確指定 encoding='utf-8',避免不同系統預設編碼造成亂碼 -path = Path("hello.txt") -with open(path, "wt", encoding="utf-8") as f: - # write() 會回傳寫入字元數;這裡不需使用回傳值 - f.write("你好,Python\n") - f.write("第二行\n") - -# 讀回:一次讀完 vs 逐行讀 -with open(path, "rt", encoding="utf-8") as f: - # f.read() 會一次把整個檔案載入記憶體 - # 適合小檔;大檔可能吃掉大量 RAM - print(f.read()) # 一次讀完(小檔才適合) - -with open(path, "rt", encoding="utf-8") as f: - # 逐行迭代:一次只讀一行,對大檔更安全、穩定 - for line in f: # 大檔必備:逐行迭代 - # line 本身通常含有尾端換行,因此用 rstrip() 去除右側空白/換行 - print(line.rstrip()) - -# ── 5.2 print 導向檔案 ───────────────────────────────── -# print(..., file=f) 可把輸出改寫到檔案,而不是終端機 -with open("log.txt", "wt", encoding="utf-8") as f: - print("登入成功", file=f) - print("使用者:", "alice", file=f) - -# ── 5.3 調整分隔符與行終止符 ─────────────────────────── -fruits = ["apple", "banana", "cherry"] -with open("fruits.csv", "wt", encoding="utf-8") as f: - # *fruits 展開清單,sep="," 指定欄位分隔 - # end="\n" 保留每次 print 結尾換行(預設本來就是 \n) - print(*fruits, sep=",", end="\n", file=f) - -# end='' 可避免多一個換行 -with open("fruits.csv", "at", encoding="utf-8") as f: - # 'at' = append text:在原檔尾端追加,不覆蓋既有內容 - print("date", end=",", file=f) - print("2026-04-23", file=f) - -# 用 Path.read_text 快速讀回整個文字檔並印出 -print(Path("fruits.csv").read_text(encoding="utf-8")) -# apple,banana,cherry -# date,2026-04-23 - -# ── 5.17 文字模式 vs 位元組模式提醒 ──────────────────── -# 'wt' 寫 str、'wb' 寫 bytes;寫錯型別會 TypeError -try: - with open("bad.txt", "wt", encoding="utf-8") as f: - # 文字模式要求 str;這裡故意傳 bytes 來示範錯誤 - f.write(b"bytes in text mode") # ← 會錯 -except TypeError as e: - # 捕捉後印出錯誤,讓教學流程能繼續執行 - print("錯誤示範:", e) diff --git a/weeks/week-09/solutions/1111405038/R02-path-and-listing.py b/weeks/week-09/solutions/1111405038/R02-path-and-listing.py deleted file mode 100644 index 2ac86f378..000000000 --- a/weeks/week-09/solutions/1111405038/R02-path-and-listing.py +++ /dev/null @@ -1,66 +0,0 @@ -# R02. 路徑操作與目錄列舉(5.11 / 5.12 / 5.13) -# Bloom: Remember — 會用 pathlib 組路徑、檢查存在、列出檔案 - -# os:傳統路徑/檔案系統 API,像 os.path.join、os.listdir -import os -# Path:pathlib 的核心類別,物件化路徑操作更直覺、可讀性更高 -from pathlib import Path - -# ── 5.11 組路徑:pathlib 是現代寫法 ──────────────────── -# 用 / 來串接路徑(不是做除法),會自動處理不同平台分隔符 -base = Path("weeks") / "week-09" - -# Path 物件常用屬性: -# - name:最後一段名稱 -# - parent:上一層路徑 -# - suffix:副檔名(含點),資料夾通常為空字串 -print(base) # weeks/week-09(Windows 會自動變成反斜線) -print(base.name) # week-09 -print(base.parent) # weeks -print(base.suffix) # ''(無副檔名) - -# 檔名相關拆解: -# - stem:去掉副檔名後的主檔名 -# - suffix:副檔名 -f = Path("hello.txt") -print(f.stem, f.suffix) # hello .txt - -# 相容舊寫法:os.path.join -# 舊專案常見,了解有助於閱讀既有程式碼 -print(os.path.join("weeks", "week-09", "README.md")) - -# ── 5.12 存在判斷 ────────────────────────────────────── -p = Path("hello.txt") - -# 三個常用檢查: -# - exists():路徑是否存在(檔案或資料夾都算) -# - is_file():是否是檔案 -# - is_dir():是否是資料夾 -print(p.exists()) # 是否存在 -print(p.is_file()) # 是否是檔案 -print(p.is_dir()) # 是否是資料夾 - -# 常見防呆:先檢查存在,再決定是否讀取/處理 -missing = Path("no_such_file.txt") -if not missing.exists(): - print(f"{missing} 不存在,略過讀取") - -# ── 5.13 列出資料夾內容 ──────────────────────────────── -# 目前工作目錄(current working directory) -here = Path(".") - -# 只列當層 -# os.listdir 回傳字串名稱,不含完整路徑 -for name in os.listdir(here): - print("listdir:", name) - -# 只抓 .py(當層) -# Path.glob 回傳 Path 物件,可直接做後續檔案操作 -for p in here.glob("*.py"): - print("glob:", p) - -# 遞迴抓所有 .py(含子資料夾) -# rglob 會深入子目錄;在大型專案中結果可能很多 -for p in Path("..").rglob("*.py"): - print("rglob:", p) - break # 示範用,只印第一個 diff --git a/weeks/week-09/solutions/1111405038/U03-bytes-and-encoding.py b/weeks/week-09/solutions/1111405038/U03-bytes-and-encoding.py deleted file mode 100644 index 0c9c4d5f2..000000000 --- a/weeks/week-09/solutions/1111405038/U03-bytes-and-encoding.py +++ /dev/null @@ -1,62 +0,0 @@ -# U03. 文字 vs 位元組、編碼觀念(5.1 encoding / 5.4) -# Bloom: Understand — 能解釋什麼時候用 'rb'、為什麼要指定 encoding - -# Path 提供直觀的路徑與檔案操作 API(write_text / write_bytes / read_text) -from pathlib import Path - -# ── 5.4 二進位讀寫:圖片、zip、任何非文字 ─────────────── -# 先造一個「假 PNG」:只寫前 8 bytes 的 magic number(檔案簽章) -# PNG 固定檔頭為:89 50 4E 47 0D 0A 1A 0A(16 進位) -# bytes([...]) 會把 0~255 的整數序列轉成位元組物件 -magic = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) - -# write_bytes:以二進位方式寫入,不涉及文字編碼 -Path("fake.png").write_bytes(magic) - -# 讀回前 8 bytes,對照 PNG 檔頭 -# 注意這裡用 'rb'(read binary):讀到的是 bytes,不是 str -with open("fake.png", "rb") as f: - head = f.read(8) - -# bytes 的印出格式會帶 b'' 前綴 -print(head) # b'\x89PNG\r\n\x1a\n' - -# 直接比較兩個 bytes 內容是否完全一致 -print(head == magic) # True - -# bytes 可逐位元組迭代(拿到 int,不是 str) -# head[:4] 只取前四個位元組,示範每個位元組可轉成十六進位顯示 -for b in head[:4]: - print(b, hex(b)) - -# ── 文字 vs 位元組的型別差 ───────────────────────────── -s = "你好" - -# encode:把 Python 字串(Unicode)依指定編碼轉成 bytes -b = s.encode("utf-8") # str → bytes - -# 型別對照:文字是 str、編碼後是 bytes -print(s, type(s)) # -print(b, type(b)) # - -# decode:把 bytes 按相同編碼還原回字串 -print(b.decode("utf-8")) # bytes → str - -# ── 5.1 encoding 參數:寫錯會爛掉 ────────────────────── -# 用 UTF-8 寫入中文文字檔 -Path("zh.txt").write_text("中文測試\n", encoding="utf-8") - -# 正常:用 utf-8 讀 utf-8 寫的檔 -print(Path("zh.txt").read_text(encoding="utf-8")) - -# 故意弄錯:用 big5 解 utf-8 → UnicodeDecodeError -# 不同編碼規則彼此不一定相容,讀取時編碼要和寫入時一致 -try: - print(Path("zh.txt").read_text(encoding="big5")) -except UnicodeDecodeError as e: - # 捕捉解碼失敗,避免程式直接中止 - print("解碼錯誤:", e) - -# 小結: -# - 文字檔 → 'rt'/'wt',一律明示 encoding='utf-8' -# - 非文字(png/zip/pickle)→ 'rb'/'wb',不談 encoding diff --git a/weeks/week-09/solutions/1111405038/U04-stringio-and-lines.py b/weeks/week-09/solutions/1111405038/U04-stringio-and-lines.py deleted file mode 100644 index e112bef46..000000000 --- a/weeks/week-09/solutions/1111405038/U04-stringio-and-lines.py +++ /dev/null @@ -1,90 +0,0 @@ -# U04. 類檔案物件 StringIO 與逐行處理(5.6 / 5.1 逐行) -# Bloom: Understand — 知道 file-like 是鴨子型別,能把記憶體當檔案用 - -# io:提供 StringIO/BytesIO,讓「記憶體中的資料」看起來像檔案物件 -import io -# Path:以物件方式操作路徑,跨平台且比字串拼接更安全 -from pathlib import Path - -# ── 5.6 StringIO:記憶體裡的「假檔案」 ───────────────── -# 建立一個空的文字緩衝區(in-memory text stream) -# 它支援 .write()、.read()、迭代等檔案常見操作 -buf = io.StringIO() - -# print(..., file=buf) 代表把輸出導向 buf,而不是終端機 -# 這和 print(..., file=open(...)) 的概念完全一致 -print("第一行", file=buf) -print("第二行", file=buf) -print("第三行", file=buf) - -# getvalue() 會一次取出目前緩衝區中的完整文字內容 -# 常用於測試:先把函式輸出寫到 StringIO,再比對字串是否正確 -text = buf.getvalue() -print("---StringIO 內容---") -print(text) - -# 寫入後,游標會停在尾端;若要「從頭讀」,要先 seek(0) -# 這和實體檔案的讀寫游標行為相同 -buf.seek(0) - -# enumerate(buf, 1):逐行迭代並從 1 開始編號 -# line 會保留行尾 '\n',所以輸出前常用 rstrip() 去掉尾端換行 -for i, line in enumerate(buf, 1): - print(i, line.rstrip()) - -# 為什麼有用?任何收 file-like 的 API(csv、json、logging) -# 都能塞 StringIO,不必真的寫到磁碟、方便測試。 -import csv - -# 第二個記憶體檔案:示範 csv.writer 也能直接寫入 StringIO -mem = io.StringIO() -writer = csv.writer(mem) - -# writerow 會自動依 CSV 規則處理欄位分隔與必要的跳脫 -writer.writerow(["name", "score"]) -writer.writerow(["alice", 90]) - -# 取出記憶體中的 CSV 文字,可直接拿去顯示、傳輸或測試比對 -print("---CSV in memory---") -print(mem.getvalue()) - -# ── 5.1 延伸:逐行處理檔案(大檔友善) ───────────────── -# 先造一個多行檔:中間刻意放空行,方便示範「過濾空白行」 -src = Path("poem.txt") - -# write_text 是 Path 的便捷 API,等價於 open(...).write(...) -# 明確指定 encoding='utf-8' 可避免跨系統編碼差異 -src.write_text("床前明月光\n\n疑是地上霜\n\n舉頭望明月\n低頭思故鄉\n", encoding="utf-8") - -# 任務:過濾空行、加上行號、寫到新檔 -dst = Path("poem_numbered.txt") - -# 同時開啟來源與目的檔案: -# - src 用 'rt' 文字讀取 -# - dst 用 'wt' 文字寫入(若已存在會覆蓋) -# 反斜線(\)是換行續寫語法,讓 with 區塊更易讀 -with open(src, "rt", encoding="utf-8") as fin, \ - open(dst, "wt", encoding="utf-8") as fout: - # n:輸出的「有效行」計數器(不含空白行) - n = 0 - - # 逐行處理是大檔實務中的基本技巧: - # 一次只保留當前行,記憶體使用量穩定 - for line in fin: - # rstrip() 去除右側空白與換行,便於判斷是否為空行 - line = line.rstrip() - - # 空字串代表空行(或只有空白),直接跳過不輸出 - if not line: - continue - - # 只有非空行才編號,讓結果連續 - n += 1 - - # {n:02d}:數字補 0 至兩位,例如 01、02、03 - # file=fout:把格式化後字串寫到目的檔 - print(f"{n:02d}. {line}", file=fout) - -# 最後讀回輸出檔,確認結果是否符合預期 -print("---加行號後---") -print(dst.read_text(encoding="utf-8")) diff --git a/weeks/week-09/solutions/1111405038/U_02_itertools.py b/weeks/week-09/solutions/1111405038/U_02_itertools.py deleted file mode 100644 index d42b94d54..000000000 --- a/weeks/week-09/solutions/1111405038/U_02_itertools.py +++ /dev/null @@ -1,99 +0,0 @@ -# Understand(理解)- itertools 工具函數 - -# 從 itertools 匯入常見工具: -# - islice:對「可迭代物件」做切片,不必先轉 list -# - dropwhile:條件成立時持續丟棄,直到第一次不成立後全部保留 -# - takewhile:條件成立時持續取用,遇到第一次不成立就停止 -# - chain:把多個可迭代物件串成一個連續序列 -# - permutations:排列(順序不同算不同) -# - combinations:組合(順序不同視為相同) -from itertools import islice, dropwhile, takewhile, chain, permutations, combinations - -print("--- islice() 切片 ---") - - -# 產生一個無限遞增的產生器(generator) -# 與 range 不同:它不會一次建立整個序列,適合示範惰性計算 -def count(n): - i = n - while True: - yield i - i += 1 - - -# 從 0 開始的無限序列:0,1,2,3,4,... -c = count(0) - -# islice(c, 5, 10) 類似序列切片 [5:10] -# 會取到第 5~9 個元素(不含 10)=> [5,6,7,8,9] -result = list(islice(c, 5, 10)) -print(f"islice(c, 5, 10): {result}") - -print("\n--- dropwhile() 條件跳過 ---") -nums = [1, 3, 5, 2, 4, 6] - -# dropwhile(lambda x: x < 5, nums) -# 流程: -# 1) 一開始只要 x < 5 就丟掉(1、3 被丟掉) -# 2) 遇到 5 時條件不成立,從這一刻起「後面全部保留」 -# 3) 所以結果是 [5,2,4,6](後面的 2、4 不會再被判斷丟棄) -result = list(dropwhile(lambda x: x < 5, nums)) -print(f"dropwhile(x<5, {nums}): {result}") - -print("\n--- takewhile() 條件取用 ---") - -# takewhile(lambda x: x < 5, nums) -# 從頭開始「只要條件成立就取」,第一個不成立就立即停止 -# 因為遇到 5 就停止,所以只會得到 [1,3] -result = list(takewhile(lambda x: x < 5, nums)) -print(f"takewhile(x<5, {nums}): {result}") - -print("\n--- chain() 串聯 ---") -a = [1, 2] -b = [3, 4] -c = [5] - -# chain(a, b, c) 不會建立中間大陣列,而是逐段迭代輸出 -# 最後轉成 list 才真正得到 [1,2,3,4,5] -print(f"chain(a, b, c): {list(chain(a, b, c))}") - -print("\n--- permutations() 排列 ---") -items = ["a", "b", "c"] -print(f"permutations(items):") - -# permutations(items) 預設 r=len(items) -# 會列出 3 個元素的所有排列,共 3! = 6 種 -for p in permutations(items): - print(f" {p}") - -print(f"permutations(items, 2):") - -# permutations(items, 2):從 3 個元素中挑 2 個並考慮順序 -# 數量為 P(3,2)=3*2=6 -for p in permutations(items, 2): - print(f" {p}") - -print("\n--- combinations() 組合 ---") -print(f"combinations(items, 2):") - -# combinations(items, 2):從 3 個元素中挑 2 個,不考慮順序 -# ('a','b') 與 ('b','a') 視為同一組,所以只有 C(3,2)=3 種 -for c in combinations(items, 2): - print(f" {c}") - -print("\n--- 組合應用:密碼窮舉 ---") -chars = ["A", "B", "1"] -print("2位數密碼:") - -# 用 permutations(chars, 2) 產生「不重複字元」的 2 位密碼 -for p in permutations(chars, 2): - print(f" {''.join(p)}") - -print("2位數密碼(可重複):") - -# combinations_with_replacement:允許重複取元素,但不考慮順序 -# 例如會有 AA、AB、A1、BB、B1、11,但不會另外出現 BA、1A... -from itertools import combinations_with_replacement - -for p in combinations_with_replacement(chars, 2): - print(f" {''.join(p)}") From 7efabe1ddb7eb0aa3b3d27cf49d2aeff430656df Mon Sep 17 00:00:00 2001 From: Python Student Date: Mon, 22 Jun 2026 19:46:56 +0800 Subject: [PATCH 3/5] feat: add red light tests for base-13 digital root - 6 test cases --- .../Base-13 Digital Root.md | 285 ++++++++++++++++++ .../test_base13_digital_root.py | 70 +++++ 2 files changed, 355 insertions(+) create mode 100644 weeks/week-18/solutions/1111405038/Base-13 Digital Root/Base-13 Digital Root.md create mode 100644 weeks/week-18/solutions/1111405038/Base-13 Digital Root/test_base13_digital_root.py diff --git a/weeks/week-18/solutions/1111405038/Base-13 Digital Root/Base-13 Digital Root.md b/weeks/week-18/solutions/1111405038/Base-13 Digital Root/Base-13 Digital Root.md new file mode 100644 index 000000000..3da4f008f --- /dev/null +++ b/weeks/week-18/solutions/1111405038/Base-13 Digital Root/Base-13 Digital Root.md @@ -0,0 +1,285 @@ +# 題目分析:任意進位的數字根(Digital Root in Base-13)- 第三題 + +## 📋 題目概述 +- **分值**:30分 +- **難度**:B題(可選擇) +- **相關資源**:week-13/QUESTION-11332.md、week-84/QUESTION-10835.md +- **技術重點**:進位轉換、數字根計算、迭代求和、模運算 + +--- + +## 🎯 題目敘述 + +### 題目描述 +數字根(Digital Root)是將一個數字的各位數字相加,若結果不是一位數則繼續相加,直到得到一位數的過程。本題要求在**任意進位制**下計算數字根,特別是 **Base=13** 的情況。 + +### 核心任務 +對輸入的每個十進位數字,依序完成: +1. **進位轉換** - 將十進位數字轉換成 Base-13 進位 +2. **計算數字根** - 迭代求和各位數字直到變成一位數 +3. **輸出結果** - 以十進位格式輸出最終的數字根 + +--- + +## 📥 輸入說明 + +``` +x₁ (0 ≤ x₁ ≤ 10⁹) +x₂ +... +xₙ +EOF +``` + +- 輸入多個十進位整數 +- 每行一個十進位數字 x(0 ≤ x ≤ 10⁹) +- 當遇到 EOF(檔案結束)時終止 + +--- + +## 📤 輸出說明 + +對每個輸入的十進位數字,輸出在 Base-13 進位下的數字根: +- 結果以十進位格式輸出 +- 每行輸入對應一行輸出 +- 一位數(0-12)直接輸出 + +--- + +## 📊 範例說明(Base = 13) + +### Sample Input +``` +0 +13 +169 +170 +``` + +### Sample Output +``` +0 +1 +1 +2 +``` + +### 詳細過程 + +#### 第1個:`0` +``` +十進位:0 +Base-13 表示:0 +位數和:0(一位數,終止) +輸出:0 +``` + +#### 第2個:`13` +``` +十進位:13 +Base-13 表示:10 (1×13¹ + 0×13⁰) +位數和:1 + 0 = 1(一位數,終止) +輸出:1 +``` + +#### 第3個:`169` +``` +十進位:169 +Base-13 表示:100 (1×13² + 0×13¹ + 0×13⁰) +位數和:1 + 0 + 0 = 1(一位數,終止) +輸出:1 +``` + +#### 第4個:`170` +``` +十進位:170 +Base-13 表示:101 (1×13² + 0×13¹ + 1×13⁰) +位數和:1 + 0 + 1 = 2(一位數,終止) +輸出:2 +``` + +### 更複雜的範例 + +#### `182` +``` +十進位:182 +Base-13 表示:110 (1×13² + 1×13¹ + 0×13⁰) + 計算:182 ÷ 13 = 14 餘 0 + 14 ÷ 13 = 1 餘 1 + 1 ÷ 13 = 0 餘 1 + 結果:110 (由下往上讀) + +第1次迭代:1 + 1 + 0 = 2(一位數,終止) +輸出:2 +``` + +#### `195` +``` +十進位:195 +Base-13 表示:120 (1×13² + 2×13¹ + 0×13⁰) + 計算:195 ÷ 13 = 15 餘 0 + 15 ÷ 13 = 1 餘 2 + 1 ÷ 13 = 0 餘 1 + 結果:120 + +第1次迭代:1 + 2 + 0 = 3(一位數,終止) +輸出:3 +``` + +#### `311` (較大的例子) +``` +十進位:311 +Base-13 表示:1 + 12×13 = 1 + 156 = 157... (計算有誤,重新計算) + 計算:311 ÷ 13 = 23 餘 12 + 23 ÷ 13 = 1 餘 10 + 1 ÷ 13 = 0 餘 1 + 結果:1(10)(12) 在 Base-13 中表示為 [1, 10, 12] + +第1次迭代:1 + 10 + 12 = 23 + 在 Base-13 中:23 = 1×13 + 10,所以 Base-13 表示為 [1, 10] + +第2次迭代:1 + 10 = 11(一位數,終止) +輸出:11 +``` + +--- + +## 🔑 關鍵要點 + +1. **進位轉換** - 使用除法和餘數得到 Base-13 各位數字 + - x % 13 得到最低位 + - x // 13 得到商,繼續處理 + +2. **一位數判定** - 在 Base-13 中,一位數的範圍是 0-12 + - 當數字小於 13 時,就是一位數 + +3. **迭代求和** - 重複計算位數和直到結果小於 13 + +4. **Base-13 數字範圍** - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 + +5. **特殊情況** - 輸入 0 的數字根為 0 + +--- + +## 💡 演算法策略 + +### 方案1:逐步轉換和求和(推薦) +```python +def digital_root_base13(n): + """計算 Base-13 進位下的數字根""" + if n == 0: + return 0 + + # 反覆計算位數和直到一位數 + while n >= 13: + digit_sum = 0 + while n > 0: + digit_sum += n % 13 + n //= 13 + n = digit_sum + + return n +``` + +### 方案2:轉換為 Base-13 後求和 +```python +def digital_root_base13_v2(n): + """先轉成 Base-13,再計算數字根""" + if n == 0: + return 0 + + while n >= 13: + # 轉換為 Base-13 並求和各位 + digit_sum = 0 + temp = n + while temp > 0: + digit_sum += temp % 13 + temp //= 13 + n = digit_sum + + return n +``` + +### 方案3:使用數學性質(高級) +```python +def digital_root_base13_math(n): + """利用模運算的性質快速計算""" + if n == 0: + return 0 + + # 在 Base-b 中,數字根有性質:dr(n) ≡ n (mod b-1) + # 但需要特殊處理使結果在 1 到 b-1 之間 + result = n % 12 + if result == 0 and n > 0: + result = 12 + + return result +``` + +--- + +## 📌 邊界情況(Edge Cases) + +- **零的數字根** - 0 應直接輸出 0 +- **單位數字** - 0-12 直接輸出(已是一位數) +- **13 的倍數** - 如 13, 26, 39... 的數字根計算 +- **大數字** - 最大到 10⁹ 的處理 +- **快速重複** - 某些數字快速收斂到同一個結果 +- **Base-13 各位數字** - 數字 0-12 的正確處理 +- **邊界值** - x=0 和 x=10⁹ 的測試 + +--- + +## 📚 相關資源參考 + +- `week-13/QUESTION-11332.md` - 進位轉換相關題目 +- `week-84/QUESTION-10835.md` - 數字根相關題目 +- 進位制轉換 - 基礎數論知識 + +--- + +## ✅ 實作檢查清單 + +- [ ] 讀取輸入直到 EOF +- [ ] 實作 Base-13 進位轉換 +- [ ] 實作位數求和邏輯 +- [ ] 正確判定一位數(< 13) +- [ ] 實作迭代求和過程 +- [ ] 驗證 Base=13 的正確性 +- [ ] 測試特殊情況(0、13、169、170 等) +- [ ] 測試所有邊界情況 +- [ ] 驗證輸出格式(十進位) +- [ ] 處理大數字(最大 10⁹) + +--- + +## 📝 Base 參數 + +**當前設定:Base = 13** + +使用此參數時: +- 一位數範圍:0-12 +- 進位關係:每 13 進為 1 +- 13 → 數字根 = 1 +- 169 (13²) → 數字根 = 1 +- 182 → 數字根 = 2 +- 195 → 數字根 = 3 + +--- + +## 🧮 Base-13 進位速查表 + +| 十進位 | Base-13 | 數字根 | +|--------|---------|--------| +| 0 | 0 | 0 | +| 1 | 1 | 1 | +| 12 | C | 12 | +| 13 | 10 | 1 | +| 14 | 11 | 2 | +| 25 | 1C | 13 → 1 | +| 26 | 20 | 2 | +| 169 | 100 | 1 | +| 170 | 101 | 2 | +| 182 | 110 | 2 | +| 195 | 120 | 3 | +| 311 | 1(10)(12) | 11 | diff --git a/weeks/week-18/solutions/1111405038/Base-13 Digital Root/test_base13_digital_root.py b/weeks/week-18/solutions/1111405038/Base-13 Digital Root/test_base13_digital_root.py new file mode 100644 index 000000000..724169575 --- /dev/null +++ b/weeks/week-18/solutions/1111405038/Base-13 Digital Root/test_base13_digital_root.py @@ -0,0 +1,70 @@ +""" +紅燈測試:任意進位的數字根(Base-13 Digital Root)- 第三題 + +測試框架:驗證 Base-13 進位下的數字根計算 +預期狀態:所有測試失敗(尚未實作解題檔) +""" + + +def run_tests(): + """執行所有紅燈測試""" + print("=" * 70) + print("開始執行紅燈測試(Red Light Tests)- Base-13 數字根") + print("=" * 70) + + test_cases = [ + # (輸入, 預期輸出, 測試描述) + (0, 0, "基本情況:0 的數字根"), + (12, 12, "邊界情況:單位數字 (< 13)"), + (13, 1, "邊界情況:13 的倍數(13¹)"), + (169, 1, "邊界情況:Base-13 完全平方(13²)"), + (170, 2, "邊界情況:簡單兩位數加一"), + (311, 11, "邊界情況:複雜迭代求和"), + ] + + passed = 0 + failed = 0 + + for i, (input_val, expected_output, description) in enumerate(test_cases, 1): + try: + # 這裡會呼叫還未實作的函數,導致失敗 + from solution import digital_root_base13 + result = digital_root_base13(input_val) + + if result == expected_output: + print(f"✓ Test Case {i} 通過:{description}") + print(f" 輸入: {input_val}") + print(f" 預期: {expected_output}") + print(f" 結果: {result}") + passed += 1 + else: + print(f"✗ Test Case {i} 失敗:{description}") + print(f" 輸入: {input_val}") + print(f" 預期: {expected_output}") + print(f" 結果: {result}") + failed += 1 + except (ImportError, ModuleNotFoundError, NameError, AttributeError) as e: + print(f"✗ Test Case {i} 失敗:{description}") + print(f" 輸入: {input_val}") + print(f" 預期: {expected_output}") + print(f" 錯誤: 解題檔未實作或函數不存在") + failed += 1 + except Exception as e: + print(f"✗ Test Case {i} 失敗:{description}") + print(f" 輸入: {input_val}") + print(f" 預期: {expected_output}") + print(f" 錯誤: {str(e)}") + failed += 1 + + print() + + print("=" * 70) + if passed == 0 and failed == len(test_cases): + print(f"❌ 紅燈測試:{failed}/{len(test_cases)} 失敗(正常,解題檔尚未實作)") + else: + print(f"✓ 綠燈測試:{passed}/{len(test_cases)} 通過") + print("=" * 70) + + +if __name__ == "__main__": + run_tests() From 288a53d74010acbbede3db4fdabdf2e3aa1999a0 Mon Sep 17 00:00:00 2001 From: Python Student Date: Mon, 22 Jun 2026 19:53:59 +0800 Subject: [PATCH 4/5] feat: add base-13 digital root solution - all tests passing (green light) --- .../Base-13 Digital Root/solution.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 weeks/week-18/solutions/1111405038/Base-13 Digital Root/solution.py diff --git a/weeks/week-18/solutions/1111405038/Base-13 Digital Root/solution.py b/weeks/week-18/solutions/1111405038/Base-13 Digital Root/solution.py new file mode 100644 index 000000000..23a4dc28e --- /dev/null +++ b/weeks/week-18/solutions/1111405038/Base-13 Digital Root/solution.py @@ -0,0 +1,57 @@ +""" +解題檔:任意進位的數字根(Base-13 Digital Root)- 第三題 + +核心任務: +1. 進位轉換 - 將十進位數字轉換成 Base-13 進位 +2. 計算數字根 - 迭代求和各位數字直到變成一位數 +3. 輸出結果 - 以十進位格式輸出最終的數字根 +""" + + +def digital_root_base13(n): + """ + 計算 Base-13 進位下的數字根 + + Args: + n: 十進位整數 + + Returns: + Base-13 進位下的數字根(十進位表示) + + 算法: + - 重複計算各位數字和直到結果小於 13 + - 利用模運算 (n % 13) 和整除 (n // 13) 進行進位轉換 + """ + if n == 0: + return 0 + + # 反覆計算位數和直到一位數(< 13) + while n >= 13: + digit_sum = 0 + # 進位轉換:提取各位數字並求和 + while n > 0: + digit_sum += n % 13 # 提取最低位數字 + n //= 13 # 移除最低位 + n = digit_sum # 更新 n 為位數和 + + return n + + +def main(): + """主程序 - 讀取輸入直到 EOF 並輸出數字根""" + try: + while True: + try: + line = input() + num = int(line) + result = digital_root_base13(num) + print(result) + except EOFError: + # 遇到 EOF 時終止 + break + except Exception as e: + pass + + +if __name__ == "__main__": + main() From e1c76459607fc49571b9786790ddf38ebbd31518 Mon Sep 17 00:00:00 2001 From: Python Student Date: Mon, 22 Jun 2026 20:08:33 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20Base-13=20?= =?UTF-8?q?=E6=95=B8=E5=AD=97=E6=A0=B9=E7=9A=84=20AI=5FLOG=20=E7=B4=80?= =?UTF-8?q?=E9=8C=84=EF=BC=8C=E5=8C=85=E5=90=AB=E4=BB=BB=E5=8B=99=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E8=88=87=E6=B8=AC=E8=A9=A6=E6=A1=88=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1111405038/Base-13 Digital Root/AI_LOG.md | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 weeks/week-18/solutions/1111405038/Base-13 Digital Root/AI_LOG.md diff --git a/weeks/week-18/solutions/1111405038/Base-13 Digital Root/AI_LOG.md b/weeks/week-18/solutions/1111405038/Base-13 Digital Root/AI_LOG.md new file mode 100644 index 000000000..148404c32 --- /dev/null +++ b/weeks/week-18/solutions/1111405038/Base-13 Digital Root/AI_LOG.md @@ -0,0 +1,135 @@ +# AI_LOG - Base-13 Digital Root 第三題 + +這份紀錄只整理本題的實作流程,並保留你下的指令原文,方便之後核對。 + +--- + +## 任務 1:整理題目並創建題目檔 + +### 你下的指令 +```text +Base=13 整理題目並創建題目檔 並創建資料夾命名放入weeks\week-18\solutions\1111405038 +``` + +### 我問 AI 什麼 +請根據照片內容整理成完整題目分析檔,並依照 Base=13 的條件建立資料夾與題目檔。 + +### AI 給了什麼 +- 完整題目敘述 +- 輸入 / 輸出規格 +- Base-13 數字根的計算說明 +- 多個範例與邊界情況 +- 3 種演算法策略 + +### 我改了什麼 +- 我確認題目重點是「Base-13 下的數字根」,不是一般十進位數字根。 +- 我補齊了 Base-13 的一位數範圍是 0 到 12。 +- 我把範例與說明整理成可直接交作業的題目檔格式。 +- 我也依照要求建立了資料夾 `Base-13 Digital Root` 放在 `weeks\week-18\solutions\1111405038` 底下。 + +--- + +## 任務 2:設計紅燈測試 + +### 你下的指令 +```text +根據要求設計一個設計一個測試 要有三個以上的test case 並進行紅燈 只做我要求的 +放到weeks\week-18\solutions\1111405038\Base-13 Digital Root +``` + +### 我問 AI 什麼 +請幫我設計至少 3 個 test case 的紅燈測試,目標是先驗證題目規格是否清楚。 + +### AI 給了什麼 +- 一個可直接執行的 Python 測試檔 +- 6 個測試案例 +- 紅燈狀態的輸出邏輯 +- 每個測試都有輸入、預期值與描述 + +### 我改了什麼 +- 我保留了 6 個測試,不只做到 3 個,讓邊界情況更完整。 +- 我選的案例有:`0`、`12`、`13`、`169`、`170`、`311`。 +- 我確認這些案例都能對應 Base-13 的核心規則,尤其是 `13` 與 `13^2` 的情況。 +- 我先不做解題檔,讓測試維持紅燈狀態,符合流程要求。 + +### 測試案例 +| # | 輸入 | 預期輸出 | 用途 | +|---|---:|---:|---| +| 1 | 0 | 0 | 零值 | +| 2 | 12 | 12 | 一位數邊界 | +| 3 | 13 | 1 | 13 的倍數 | +| 4 | 169 | 1 | 13^2 | +| 5 | 170 | 2 | 相鄰值 | +| 6 | 311 | 11 | 複雜迭代 | + +--- + +## 任務 3:做出解題檔後測綠燈 + +### 你下的指令 +```text +做出解題檔 後測綠燈 只做我要求的 +``` + +### 我問 AI 什麼 +請實作 `solution.py`,讓前面設計的紅燈測試全部通過。 + +### AI 給了什麼 +- `digital_root_base13(n)` 的實作 +- `main()` 讀取 EOF 輸入並逐行輸出 +- 使用 `n % 13` 和 `n // 13` 做進位拆解 +- 以 `n < 13` 作為一位數終止條件 + +### 我改了什麼 +- 我照 Base-13 的規則實作數字根,不是直接用十進位數字根公式。 +- 我確認 `0` 要直接回傳 `0`。 +- 我確認 `n >= 13` 時要反覆把各位數字相加,直到結果小於 13。 +- 我用先前那 6 個測試實際驗證,結果全部通過,確認綠燈成立。 + +### 綠燈結果 +- `0 -> 0` +- `12 -> 12` +- `13 -> 1` +- `169 -> 1` +- `170 -> 2` +- `311 -> 11` + +--- + +## 任務 4:完整 AI_LOG + +### 你下的指令 +```text +完整AI_LOG +並且要有有我的下的指令確實記錄 +``` + +### 我問 AI 什麼 +請把整個題目的流程整理成完整 AI_LOG,並明確保留我下的指令內容。 + +### AI 給了什麼 +- AI_LOG 的章節架構 +- 任務式整理方式 +- 建議保留「我問 AI 什麼 / AI 給了什麼 / 我改了什麼」三欄 + +### 我改了什麼 +- 我把每次你下的指令原文都放進 AI_LOG。 +- 我把每一階段的目的、輸出與我自己的判斷都寫清楚。 +- 我保留了紅燈與綠燈流程,方便檢查不是只有答案,而是有完整過程。 +- 我也補上測試案例與結果,讓這份 AI_LOG 可以直接對照題目流程。 + +--- + +## 最後確認 + +本題目前已完成: +- 題目分析檔 +- 紅燈測試檔 +- 解題檔 +- 完整 AI_LOG + +這份 AI_LOG 的重點是: +1. 保留你的原始指令 +2. 記錄 AI 給了什麼 +3. 說明我實際改了什麼 +4. 能對照紅燈到綠燈的整個過程