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
26 changes: 25 additions & 1 deletion weeks/week-14/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
# Week 14(115/05/25-115/05/31)

- 主題:綜合練習
- 主題:測試、除錯與例外 + 綜合練習
- 課堂範例:[`in_class/`](./in_class/) — Python3 Cookbook 第 14 章,依 Bloom's Taxonomy 分為 R/U 兩層共 5 個範例
- 解題:[11349](./QUESTION-11349.md) | [11417](./QUESTION-11417.md) | [11461](./QUESTION-11461.md) | [12019](./QUESTION-12019.md)
- 作業:完成 4 題並提交到 `weeks/week-14/solutions/<student-id>/`

---

## 課堂範例(in_class/)

涵蓋教材:[Python3 Cookbook 第 14 章:測試、調試和異常](https://python3-cookbook.readthedocs.io/zh-cn/latest/chapters/p14_test_debug_and_exceptions.html)

### 記憶層(R — Remember)

| 檔案 | 涵蓋節次 | 主題 |
|------|----------|------|
| [R01](./in_class/R01-unittest-basics.py) | 14.1–14.3 | unittest 基礎:`redirect_stdout` / `mock.patch` / `assertRaises` |
| [R02](./in_class/R02-exceptions-basic.py) | 14.6–14.8 | 例外處理基本:多例外 tuple / `except Exception` / 自定義例外 |
| [R03](./in_class/R03-profile-basic.py) | 14.13 | 效能測量:`timed` 裝飾器 / `timeit` / `cProfile` |

### 理解層(U — Understand)

| 檔案 | 涵蓋節次 | 主題 |
|------|----------|------|
| [U01](./in_class/U01-test-warnings-why.py) | 14.4, 14.5, 14.11 | 測試控制與警告:`skipIf` 的報表價值 / `stacklevel=2` / Warning 種類選擇 |
| [U02](./in_class/U02-debug-speedup-why.py) | 14.9, 14.10, 14.12, 14.14 | 例外、除錯與加速:四種 raise 寫法對照 / `print_exc` 必要性 / `LOAD_FAST` vs `LOAD_GLOBAL` |

詳細說明見 [`in_class/README.md`](./in_class/README.md)。

---

## 解題清單

| # | 題名 | 難度 | 題目檔 |
Expand Down
74 changes: 74 additions & 0 deletions weeks/week-14/in_class/R01-unittest-basics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
R01:unittest 基本用法(記憶層 — 直接複製可執行)

對應 Cookbook:
- 14.1 測試 stdout 輸出
- 14.2 在單元測試中給物件打補丁
- 14.3 在單元測試中測試例外情況

執行:
python R01-unittest-basics.py
"""
import io
import unittest
from contextlib import redirect_stdout
from unittest.mock import MagicMock, patch


# ---------- 被測函式 ----------
def url_print(host, domain):
print(f"https://{host}.{domain}")


def parse_int(s):
if not s:
raise ValueError("空字串無法轉成整數")
return int(s)


def fetch_user(api, user_id):
return api.get(f"/users/{user_id}")


# ---------- 14.1 測試 stdout ----------
class TestStdout(unittest.TestCase):
def test_url_print(self):
buf = io.StringIO()
with redirect_stdout(buf):
url_print("www", "example.com")
self.assertEqual(buf.getvalue().strip(), "https://www.example.com")


# ---------- 14.2 mock.patch ----------
class TestPatch(unittest.TestCase):
def test_fetch_user_with_mock(self):
fake_api = MagicMock()
fake_api.get.return_value = {"id": 1, "name": "Alice"}

result = fetch_user(fake_api, 1)

self.assertEqual(result["name"], "Alice")
fake_api.get.assert_called_once_with("/users/1")

@patch("builtins.print")
def test_url_print_via_patch(self, mock_print):
url_print("api", "example.com")
mock_print.assert_called_once_with("https://api.example.com")


# ---------- 14.3 測試例外 ----------
class TestExceptions(unittest.TestCase):
def test_raises(self):
with self.assertRaises(ValueError):
parse_int("")

def test_raises_with_message(self):
with self.assertRaisesRegex(ValueError, "空字串"):
parse_int("")

def test_normal_case(self):
self.assertEqual(parse_int("42"), 42)


if __name__ == "__main__":
unittest.main()
73 changes: 73 additions & 0 deletions weeks/week-14/in_class/R02-exceptions-basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
R02:例外處理基本用法(記憶層)

對應 Cookbook:
- 14.6 處理多個例外
- 14.7 捕獲所有例外
- 14.8 建立自定義例外

執行:
python R02-exceptions-basic.py
"""
import traceback


# ---------- 14.6 多個例外 ----------
def parse_value(s):
"""同一個 except 用 tuple 列出多種例外類別"""
try:
return int(s)
except (ValueError, TypeError) as e:
print(f"[14.6] 解析失敗 {type(e).__name__}: {e}")
return None


# ---------- 14.7 捕獲所有例外 ----------
def safe_run(func, *args):
"""except Exception,而不是裸 except:(裸 except 會抓到 KeyboardInterrupt)"""
try:
return func(*args)
except Exception as e:
print(f"[14.7] 發生例外 {type(e).__name__}: {e}")
traceback.print_exc()


# ---------- 14.8 自定義例外 ----------
class NetworkError(Exception):
"""所有網路錯誤的基底類別;繼承 Exception 而不是 BaseException"""


class HostnameError(NetworkError):
"""找不到主機"""


class ConnectionTimeout(NetworkError):
"""連線逾時,附帶 host / seconds 屬性,方便上層判斷"""
def __init__(self, host, seconds):
super().__init__(f"連線 {host} 超過 {seconds} 秒")
self.host = host
self.seconds = seconds


def connect(host, timeout):
if host == "":
raise HostnameError("主機名稱為空")
if timeout < 1:
raise ConnectionTimeout(host, timeout)
return f"connected to {host}"


if __name__ == "__main__":
print("--- 14.6 ---")
parse_value("abc")
parse_value(None)

print("\n--- 14.7 ---")
safe_run(lambda: 1 / 0)

print("\n--- 14.8 ---")
for host, t in [("example.com", 5), ("", 5), ("slow.com", 0)]:
try:
print(connect(host, t))
except NetworkError as e:
print(f"接到 {type(e).__name__}: {e}")
65 changes: 65 additions & 0 deletions weeks/week-14/in_class/R03-profile-basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""
R03:效能測量基本用法(記憶層)

對應 Cookbook:
- 14.13 給程式做效能測試(time / timeit / cProfile)

執行:
python R03-profile-basic.py
"""
import cProfile
import math
import pstats
import time
import timeit
from functools import wraps


# ---------- 計時裝飾器(粗粒度) ----------
def timed(func):
@wraps(func)
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
print(f"[timed] {func.__name__}: {elapsed*1000:.2f} ms")
return result
return wrapper


@timed
def sum_of_squares(n):
return sum(i * i for i in range(n))


# ---------- timeit:量微小片段 ----------
def bench_timeit():
n = 10_000
t1 = timeit.timeit("sum(i*i for i in range(n))",
globals={"n": n}, number=1000)
t2 = timeit.timeit("sum(map(lambda i: i*i, range(n)))",
globals={"n": n}, number=1000)
print(f"[timeit] genexp = {t1:.3f}s, map+lambda = {t2:.3f}s")


# ---------- cProfile:找熱點 ----------
def workload():
total = 0
for i in range(1, 5000):
total += math.sqrt(i) * math.sin(i)
return total


def bench_cprofile():
pr = cProfile.Profile()
pr.enable()
workload()
pr.disable()
print("[cProfile] 前 5 名:")
pstats.Stats(pr).sort_stats("cumulative").print_stats(5)


if __name__ == "__main__":
sum_of_squares(1_000_000)
bench_timeit()
bench_cprofile()
55 changes: 55 additions & 0 deletions weeks/week-14/in_class/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Week 14 課堂範例:測試、除錯與例外

本目錄涵蓋:[Python3 Cookbook 第 14 章:測試、調試和異常](https://python3-cookbook.readthedocs.io/zh-cn/latest/chapters/p14_test_debug_and_exceptions.html)

依 **Bloom's Taxonomy** 分為兩層,整理成 5 個範例:

- **R(Remember 記憶層)**:直接可複製、可執行的基礎用法
- **U(Understand 理解層)**:為什麼這樣做?陷阱、抉擇與背後原理

---

## 章節對照表

### 記憶層(R)

| 範例 | 涵蓋節次 | 主題 | 核心 API |
|------|----------|------|----------|
| [R01](./R01-unittest-basics.py) | 14.1–14.3 | unittest 基礎 | `redirect_stdout` / `mock.patch` / `assertRaises` |
| [R02](./R02-exceptions-basic.py) | 14.6–14.8 | 例外處理基本 | 多例外 tuple / `except Exception` / 自定義例外類別 |
| [R03](./R03-profile-basic.py) | 14.13 | 效能測量 | `timed` 裝飾器 / `timeit` / `cProfile` |

### 理解層(U)

| 範例 | 涵蓋節次 | 主題 | 關鍵概念 |
|------|----------|------|----------|
| [U01](./U01-test-warnings-why.py) | 14.4, 14.5, 14.11 | 測試控制與警告的「為什麼」 | `skipIf` vs `if return` / `expectedFailure` 的價值 / `stacklevel=2` / Warning 種類選擇 |
| [U02](./U02-debug-speedup-why.py) | 14.9, 14.10, 14.12, 14.14 | 例外、除錯與加速的「為什麼」 | `raise from` vs `raise X` vs bare `raise` vs `raise e` / `print_exc` vs `print(e)` / `LOAD_FAST` vs `LOAD_GLOBAL` |

---

## 執行方式

```bash
cd weeks/week-14/in_class

# 記憶層
python R01-unittest-basics.py
python R02-exceptions-basic.py
python R03-profile-basic.py

# 理解層
python U01-test-warnings-why.py
python U01-test-warnings-why.py --log # 把測試結果寫到 test_result.log
python U02-debug-speedup-why.py
```

---

## 學習重點(一句話帶走)

- **R01**:mock 掉外部相依、用 `assertRaises` 驗證例外、用 `redirect_stdout` 驗證輸出。
- **R02**:多例外用 tuple;`except Exception` 不要寫成 bare `except:`;自定義例外繼承 `Exception`。
- **R03**:粗測用 `time.perf_counter`,微測用 `timeit`,找瓶頸用 `cProfile`。
- **U01**:`skipIf` 讓報表能看出「為什麼沒測」;`warnings.warn` 必加 `stacklevel=2` 指向呼叫端。
- **U02**:包裝底層錯誤用 `raise X from e`;中途記 log 再拋用 bare `raise`;**不要寫 `raise e`**,那會丟失 traceback。
Loading
Loading