Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions weeks/week-16/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Week 16(115/06/08-115/06/14)

- 主題:**TDD + Git PR 加壓演練週**(期末上機考倒數第二個練習週)
- 課堂範例:[`in_class/`](./in_class/) — 6/10 計時演練、6/11 獨立演練
- 課堂範例:[`in_class/`](./in_class/) — 6/10 計時演練、6/11 排序效能專題
- 解題:本週無 `QUESTION-*.md`
- 作業:依 6/10、6/11 課堂教案完成 PR,**兩天都必須附 `AI_LOG.md`**
- 備註:6/12(五)生成式 AI 論壇,本週除兩個課堂 PR 外無額外作業
- 作業:依 6/10、6/11 課堂教案完成 PR,**兩天都必須附 `AI_LOG.md`**;6/11 專題的 Stage 3–4 課後完成,**6/17(三)前補進同一個 PR**
- 備註:6/12(五)生成式 AI 論壇

---

Expand All @@ -28,10 +28,10 @@ TDD 原則不變:**先測試(紅燈)→ 再實作(綠燈)**,commit
| 日期 | 教案 | 題目 | 是否評分 |
|------|------|------|---------|
| 6/10 (三) | [0610-timed-drill.md](./in_class/0610-timed-drill.md) | 數字根(UVA 11332 簡化版) | 不評分,記錄是否準時開出 PR |
| 6/11 (四) | [0611-solo-drill.md](./in_class/0611-solo-drill.md) | 進位計數(UVA 10035 簡化版) | 不評分,記錄 PR 與 AI_LOG 完整度 |
| 6/11 (四) | [0611-sort-lab.md](./in_class/0611-sort-lab.md) | 排序效能實驗室(timeit 裝飾器、三種排序、加速、視覺化) | **納入作業成績**(評分表見教案) |

> 本週兩題均進入期末考 **B 區候選池(不公開)**。
> B 區範圍是 6/3 ~ 6/18 的週三、週四課堂題——**缺這兩堂,B 區就少一半。**
> 6/10 題目進入期末考 **B 區候選池(不公開)**;6/11 專題的 timeit 與排序屬 in-class 範圍
> B 區範圍是 6/3 ~ 6/18 的週三、週四課堂題——**缺課,B 區就少一塊。**

---

Expand Down
10 changes: 8 additions & 2 deletions weeks/week-16/in_class/0611-solo-drill.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,14 @@ PR 開出後,先**不翻文件**回答這五題(答得出來=考場上不

---

## starter 檔
## 工作目錄

本教案不提供 starter,自己建立(這也是考場上的第一步):

```bash
cp -r weeks/week-16/in_class/0611-starter weeks/week-16/solutions/<學號>/0611
mkdir -p weeks/week-16/solutions/<學號>/0611-carry
cd weeks/week-16/solutions/<學號>/0611-carry
```

從空的 `test_carry_counter.py` 開始:先補 ≥3 個 test case(含 edge case 與例外案例)、
跑紅燈、commit,再建立 `carry_counter.py`。
Comment on lines +97 to +98
157 changes: 157 additions & 0 deletions weeks/week-16/in_class/0611-sort-lab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# 6/11 (四)|分階段專題:排序效能實驗室

> 把 Week 10 的 `@timeit` 裝飾器、Week 13 的視覺化、Week 14-15 的 TDD + PR 流程,
> 整合成一個完整的小專題。**每個階段都要先紅燈再綠燈,commit 順序就是你的開發證據。**

## 學習目標

- 用 TDD 開發一個可被測試的 `@timeit` 計時裝飾器
- 實作 bubble / quick / merge 三種排序,並用自己的裝飾器量測效能
- 設計至少一種加速方案(Cython 化或演算法優化),用數據證明有效
- 把實驗結果畫成圖,寫出可信的效能報告

---

## 專題總覽(四個階段)

| 階段 | 產出 | 紅燈 commit | 綠燈 commit |
|------|------|------------|------------|
| 1 | `timing.py` — `@timeit` 裝飾器 | `test: stage1 timeit 裝飾器測試` | `feat: stage1 實作 timeit 裝飾器` |
| 2 | `sorts.py` + `benchmark.py` — 三種排序與量測 | `test: stage2 排序正確性測試` | `feat: stage2 實作三種排序與 benchmark` |
| 3 | 加速版排序(Cython 或演算法優化) | `test: stage3 加速版共用正確性測試` | `feat: stage3 加速版與量測數據` |
| 4 | `plot.py` + `assets/benchmark.png` | `test: stage4 繪圖輸出測試` | `feat: stage4 實驗結果圖表與報告` |

`git log --reverse` 必須能看到 **test → feat 交替出現八次**(可穿插 `refactor:`)。
任何一個階段「一開始就綠」或「先 feat 後 test」,該階段流程分 0 分。

---

## Stage 1|`@timeit` 裝飾器(課堂 0:00–0:25)

寫 `timing.py`,提供裝飾器 `timeit`,規格:

- 被裝飾函式的**回傳值不變**
- 用 `functools.wraps` 保留 `__name__` / `__doc__`
- 每次呼叫後,把耗時(秒,`float`)記錄在:
- `f.last_elapsed` — 最近一次的耗時
- `f.records` — 歷次耗時的 list(累積)
- 裝飾器內**不准 `print`**——輸出格式化是 benchmark 的事,計時器保持安靜才能被重複利用

> 設計提示:`last_elapsed` 和 `records` 掛在 wrapper 函式物件上。
> 想想看:為什麼掛在 wrapper 上而不是用全域變數?(這題會出現在課末檢測)

**必備 test case(≥3)**:回傳值不變、`__name__` 保留、呼叫後 `last_elapsed > 0` 且 `records` 長度遞增。

## Stage 2|三種排序 + 量測(課堂 0:25–1:10)

寫 `sorts.py`,三個函式簽名固定:

```python
def bubble_sort(data: list) -> list: ...
def quick_sort(data: list) -> list: ...
def merge_sort(data: list) -> list: ...
```

- 一律**回傳新的 list,不可修改傳入的 list**(測試會驗)
- **禁用** `sorted()` / `list.sort()`——那是 Stage 3 的對照組

**必備 test case(每個函式都要過)**:空 list、單元素、含重複值、反向排序、隨機資料與 `sorted()` 結果比對、原 list 未被修改。
三個函式共用同一組測試——用迴圈 + `subTest`,不要複製貼上三份。

接著寫 `benchmark.py`:

```python
def make_data(n: int, seed: int = 42) -> list: ... # 固定 seed,實驗可重現
def run_benchmark(sizes=(500, 1000, 2000, 4000), repeats=3) -> dict: ...
```

- 用你自己的 `@timeit` 量測,每個 n 重複 `repeats` 次取 `records` 平均
- `python benchmark.py` 印出比較表,並把結果存成 `results.json`(Stage 4 的輸入)

## Stage 3|加速實驗(課後)

1. **必做**:把內建 `sorted()`(Timsort,C 實作)加入 benchmark 當 baseline
2. **至少一種加速方案**,自選:
- **Cython 化**:把任一排序改寫成 `sorts_fast.pyx`(加上 `cdef` 型別註記),
用 `pip install cython` + `setup.py build_ext --inplace` 或 `pyximport` 編譯
- **演算法優化**:bubble 提前停止、quick 改 median-of-three、小區間切換插入排序等
3. 加速版**必須通過 Stage 2 同一組正確性測試**(把被測函式做成參數,別再寫一份測試)
4. 加速前後的數據都要進 `results.json`,報告要寫出加速比(例:`bubble 4000 筆:2.31s → 0.18s,12.8x`)

> ⚠️ Cython 編譯產物 `build/`、`*.c`、`*.so` **不准 commit**——只交 `.pyx` 原始碼與編譯指令說明。

## Stage 4|畫圖與報告(課後)

寫 `plot.py`:

```python
def load_results(path: str) -> dict: ...
def plot_results(results: dict, out_path: str) -> None: ...
```

- 折線圖:x 軸 = 資料量 n,y 軸 = 平均秒數(**y 軸用 log scale**,不然 O(n²) 會把其他線壓扁)
- 每個演算法一條線(含 baseline 與加速版),輸出 `assets/benchmark.png`
- 測試提示:用 `tempfile` 驗證 PNG 有產生且大小 > 0;`plot.py` 開頭加 `matplotlib.use("Agg")` 才能在無視窗環境(含 CI)跑
- 在你的 `README.md` 貼圖並用 2–3 句解讀:誰最快?O(n²) 和 O(n log n) 的線斜率差在哪?加速比多少?

---

## 時程與繳交

| 時間 | 內容 |
|------|------|
| 6/11 課堂(90 分鐘) | Stage 1、2 完成並 push;**下課前開出 PR**(之後同分支繼續 push) |
| 課後 | Stage 3、4 |
| **6/17(三)前** | PR 補完全部四階段,完成最終版 |

- 分支:`feature/wk16-0611-<學號>`,PR 標題 `Week 16 - <學號> - <姓名>`
- 所有檔案只能放在 `weeks/week-16/solutions/<學號>/0611/`(CI 會檢查)

### 繳交內容清單

```
weeks/week-16/solutions/<學號>/0611/
├── timing.py test_timing.py
├── sorts.py test_sorts.py
├── benchmark.py results.json
├── sorts_fast.pyx(或演算法優化版)
├── plot.py test_plot.py
├── assets/benchmark.png
├── README.md # 實驗報告:方法、數據表、圖、解讀、加速比
├── AI_LOG.md # 提示詞逐字記錄(規則同 6/10)
└── TEST_LOG.md # 每階段至少一紅一綠的 unittest 輸出
```

### 評分(納入作業成績)

| 項目 | 配分 |
|------|------|
| Stage 1 timeit 正確 + 測試完整 | 15 |
| Stage 2 三排序正確 + 共用測試 + benchmark 可重現 | 25 |
| Stage 3 加速有效 + 通過共用測試 + 數據佐證 | 10 |
| Stage 4 圖表正確 + 解讀合理 | 10 |
| Code Style(命名、繁中註解、無重複測試碼) | 20 |
| 報告與紀錄(README / AI_LOG / TEST_LOG / commit 順序) | 20 |

---

## 課末自我檢測(不翻文件回答)

1. `last_elapsed` 為什麼掛在 wrapper 上,不用全域變數?
2. 為什麼測試要驗「原 list 未被修改」?哪個排序最容易不小心改到?
3. benchmark 為什麼要固定 seed、重複多次取平均?
4. 你的加速方案如果只跑 n=500 看不出差異,該怎麼設計實驗?
5. 八個 commit 的順序是什麼?哪一種順序會直接 0 流程分?

---

## AI 使用規則(同 6/10)

提示詞自己打、逐字記入 `AI_LOG.md`;「我改了什麼」0 字 = 期末考此項 0 分。
AI 給的測試齊不齊、排序對不對、圖正不正確——**你自己驗收**,驗收標準寫進 AI_LOG。

## starter 檔

```bash
cp -r weeks/week-16/in_class/0611-sort-starter weeks/week-16/solutions/<學號>/0611
```
54 changes: 54 additions & 0 deletions weeks/week-16/in_class/0611-sort-starter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 6/11 Starter — 排序效能實驗室

## 使用方式

```bash
cp -r weeks/week-16/in_class/0611-sort-starter weeks/week-16/solutions/<學號>/0611
cd weeks/week-16/solutions/<學號>/0611
```

## 檔案說明

- `test_timing.py`:Stage 1 測試骨架,**先補齊測試、跑紅燈、commit,再寫 `timing.py`**
- `test_sorts.py`:Stage 2 測試骨架,三種排序共用同一組測試(用 `subTest`)
- 其餘檔案(`timing.py`、`sorts.py`、`benchmark.py`、`plot.py`…)都是**紅燈 commit 之後**才建立
- 完成後追加 `AI_LOG.md`(範本見 [`week-15/in_class/ai-log-template.md`](../../../week-15/in_class/ai-log-template.md))與 `TEST_LOG.md`

## 規格速查

### Stage 1 `timing.py`

```python
def timeit(func): ...
```

- 回傳值不變;`functools.wraps` 保留 metadata
- `f.last_elapsed`:最近一次耗時(float 秒);`f.records`:歷次耗時 list
- 裝飾器內不准 `print`

### Stage 2 `sorts.py` + `benchmark.py`

```python
def bubble_sort(data: list) -> list: ...
def quick_sort(data: list) -> list: ...
def merge_sort(data: list) -> list: ...

def make_data(n: int, seed: int = 42) -> list: ...
def run_benchmark(sizes=(500, 1000, 2000, 4000), repeats=3) -> dict: ...
```

- 排序一律回傳新 list、不可改動輸入;禁用 `sorted()` / `list.sort()`
- 函式名、簽名都不能改,否則測試 import 會失敗
- `python benchmark.py` 要印出比較表並產生 `results.json`

### Stage 3 / Stage 4

見 [`../0611-sort-lab.md`](../0611-sort-lab.md):加入 `sorted()` baseline、
至少一種加速方案(Cython 或演算法優化)、`plot.py` 畫圖輸出 `assets/benchmark.png`。

## 本日規則

- [ ] 每階段先紅燈 commit(`test:`)再綠燈 commit(`feat:`),共八個 commit
- [ ] AI 提示詞自己打,逐字記入 `AI_LOG.md`
- [ ] 課堂結束前完成 Stage 1–2 並開出 PR;Stage 3–4 同分支補 push,6/17(三)前完成
- [ ] Cython 編譯產物(`build/`、`*.c`、`*.so`)不准 commit
45 changes: 45 additions & 0 deletions weeks/week-16/in_class/0611-sort-starter/test_sorts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Stage 2 — 排序正確性測試骨架

規格:sorts.py 的 bubble_sort / quick_sort / merge_sort 必須
1. 回傳新的排序後 list,不可修改傳入的 list
2. 禁用內建 sorted() / list.sort()(那是 Stage 3 的對照組)

設計要求:三個函式共用同一組測試——用迴圈 + subTest,不要複製貼上三份。

待辦:
1. 自己打提示詞跟 AI 討論,補齊測試:
空 list、單元素、重複值、反向、隨機資料比對、原 list 未被修改
2. 跑 `python -m unittest` 確認全紅
3. commit: "test: stage2 排序正確性測試"
4. 寫 sorts.py,全綠後 commit: "feat: stage2 實作三種排序與 benchmark"
"""

import unittest

# from sorts import bubble_sort, quick_sort, merge_sort # 完成 sorts.py 後解除註解

# 提示:把三個排序函式放進一個 list,每個測試用
# for sort_fn in SORT_FUNCTIONS:
# with self.subTest(sort_fn.__name__):
# ...
# Stage 3 的加速版只要 append 進這個 list 就能吃到同一組測試。
SORT_FUNCTIONS = [] # 解除上面 import 後填入


class TestSortFunctions(unittest.TestCase):
def test_basic_cases(self):
# 提示:空 list、單元素、重複值、反向排序
self.fail("尚未實作 — 自己打提示詞跟 AI 討論後補上")

def test_random_data_matches_builtin(self):
# 提示:random.seed 固定後產生資料,結果與 sorted() 比對
# (測試裡可以用 sorted() 當「驗證標準」,被測函式裡不行)
self.fail("尚未實作")

def test_input_not_mutated(self):
# 提示:排序前先複製一份,排序後驗證原 list 沒變
self.fail("尚未實作")


if __name__ == "__main__":
unittest.main()
36 changes: 36 additions & 0 deletions weeks/week-16/in_class/0611-sort-starter/test_timing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Stage 1 — @timeit 裝飾器測試骨架

規格:timing.py 的 timeit 裝飾器必須
1. 不改變被裝飾函式的回傳值
2. 用 functools.wraps 保留 __name__ / __doc__
3. 每次呼叫後更新 f.last_elapsed(float 秒)並 append 到 f.records
4. 裝飾器內不准 print

待辦:
1. 自己打提示詞跟 AI 討論,補齊下面三個測試(可再加)
2. 跑 `python -m unittest` 確認全紅
3. commit: "test: stage1 timeit 裝飾器測試"
4. 寫 timing.py,全綠後 commit: "feat: stage1 實作 timeit 裝飾器"
"""

import unittest

# from timing import timeit # 完成 timing.py 後解除註解


class TestTimeit(unittest.TestCase):
def test_returns_original_result(self):
# 提示:裝飾一個簡單函式(例如回傳 a + b),驗證回傳值不變
self.fail("尚未實作 — 自己打提示詞跟 AI 討論後補上")

def test_preserves_function_metadata(self):
# 提示:驗證 __name__ 不是 'wrapper'
self.fail("尚未實作")

def test_records_elapsed_time(self):
# 提示:呼叫兩次,驗證 last_elapsed > 0 且 records 長度為 2
self.fail("尚未實作")


if __name__ == "__main__":
unittest.main()
45 changes: 0 additions & 45 deletions weeks/week-16/in_class/0611-starter/README.md

This file was deleted.

Loading
Loading