🚀 Day 2:打造「結構化帳務明細與分類統計分析器」
5 天全日營隊 (Day 2 / 總時數 6 Hours) | 從單筆紀錄邁向資料庫二維結構
📖 為什麼學這些?——「從單筆開銷到發票分析器」關卡冒險故事線
昨天我們的記帳程式只能記錄「單一筆花費」,算完一次就失憶了。但真實生活中你一天會有好幾筆消費!今天我們要幫理財咖啡館打造**二維資料結構與數據分析系統**:
| 單元名稱 | 記帳程式關卡角色 | 為什麼這個單元不可或缺?(技術關聯) |
|---|---|---|
| 單元 1:List 基礎與操作 | 🛒 裝發票的購物車口袋 | 使用清單(List)有序儲存多筆消費,隨時用 append() 把剛買的物品塞進袋子裡。 |
| 單元 2:Dict 鍵值對 | 🏷️ 商品詳細成分標籤牌 | 單靠數字無法說明細節。字典(Dict)能用自訂 Key-Value 打包商品的「名稱、價格、分類」詳細屬性。 |
| 單元 3:List of Dicts 組合 | 📊 完整帳本數據表 (Rows/Table) | 將「字典清單化」二維組合 [{...}, {...}],完美模擬現代資料庫表格(Database Table)的每一列記錄。 |
| 單元 4:for 迴圈走訪統計 | 🧮 自動總帳巡邏機器人 | 手動加總太慢了!for 迴圈能自動逐筆巡邏所有紀錄,精確計算出總消費金額與單筆平均開銷。 |
| 單元 5:擴充資料模型 Schema | 🗓️ 帳本欄位升級 (Schema) | 為資料模型導入 category 與系統自動日期 datetime,讓帳本轉變為專業軟體數據規格。 |
| 單元 6:黑客松:資料分類器 | 🔍 精明消費分析師 (ORM filter) | 實作條件篩選:一鍵掏出所有「餐飲」或「娛樂」消費小計,銜接未來 Django ORM 的 .filter() 查詢! |
📌 課程前導與簡介說明
- 營隊定位: Day 2 的任務是引導學生理解如何將真實世界的「一張帳單明細」透過 List 與 Dict 結構化表達。
- 具象對映法:清單(List)等於購物車,字典(Dict)等於商品的標籤牌(名稱、金額、分類)。
- Django 銜接點:字典的 Key 就是資料庫欄位(Column),包含多個字典的清單(List of Dicts)就是一張資料庫數據表(Table)或 QuerySet。
歡迎來到 Day 2!今天我們要解鎖「資料結構化」的魔法!請閱讀每個單元的 **故事單元說明**,掌握 List 與 Dict 的特性。點擊單元內的 **延伸閱讀** 展開技術深度觀念與 Google 搜尋,遇到寫碼難題時使用專屬的 **AI 協作除錯提示詞**!
💻 Day 2 專案總成果:結構化帳務統計與分類分析器
【教師參考解答】學生於 Day 2 結束時將能獨立撰寫的完整程式:
"""
Day 2 最終成果:結構化帳務明細與分類統計分析器
核心概念:List of Dicts 資料結構 -> for 迴圈走訪加總 -> 條件篩選 (ORM .filter 雛形)
"""
import datetime
# 1. 初始化資料結構 (模擬 Database Table / QuerySet)
records = [
{"name": "珍珠奶茶", "price": 60, "category": "餐飲", "date": "2026-08-03"},
{"name": "Python 教科書", "price": 550, "category": "學習", "date": "2026-08-03"},
{"name": "排骨便當", "price": 110, "category": "餐飲", "date": "2026-08-03"}
]
print("==========================================")
print(" 📊 Day 2 結構化帳務統計與篩選器 📊 ")
print("==========================================")
# 2. 新增一筆動態紀錄
new_item = input("👉 請輸入新增消費名稱: ").strip()
new_price = int(input("👉 請輸入金額 (TWD): "))
new_cat = input("👉 請輸入分類 (餐飲/學習/娛樂): ").strip()
today_str = datetime.date.today().strftime("%Y-%m-%d")
new_record = {
"name": new_item,
"price": new_price,
"category": new_cat if new_cat else "一般",
"date": today_str
}
records.append(new_record)
# 3. 走訪清單並計算統計資訊
print("\n---------------- 🛒 完整消費明細表 ----------------")
total_sum = 0
for idx, item in enumerate(records, start=1):
print(f"[{idx}] {item['date']} | {item['name']:<10} | ${item['price']:<5} | 分類: {item['category']}")
total_sum += item["price"]
avg_price = total_sum / len(records)
print("----------------------------------------------------")
print(f"💰 總消費金額 :${total_sum} 元")
print(f"📈 平均單筆花費:${avg_price:.1f} 元")
# 4. 條件篩選 (模擬 Django ORM .filter())
search_cat = input("\n🔍 請輸入要篩選的分類名稱 (例如: 餐飲): ").strip()
print(f"\n--- 🔎 分類「{search_cat}」篩選結果 ---")
filtered_count = 0
filtered_sum = 0
for item in records:
if item["category"] == search_cat:
print(f" • {item['name']}: ${item['price']} 元 ({item['date']})")
filtered_count += 1
filtered_sum += item["price"]
if filtered_count > 0:
print(f"\n🎯 符合「{search_cat}」共 {filtered_count} 筆,小計:${filtered_sum} 元")
else:
print(f"\n⚠️ 找不到分類為「{search_cat}」的消費紀錄。")
【學生自主實作框架】請根據今日各單元所學,將空缺的 `____` 填入正確程式碼:
"""
🎯 Day 2 自主挑戰:請將下方的 ____ 填入正確的 List、Dict 或 for 迴圈語法!
"""
import datetime
# 1. 初始化資料庫結構 (List of Dicts)
records = [
{"name": "珍珠奶茶", "price": 60, "category": "餐飲"},
{"name": "Python 教科書", "price": 550, "category": "學習"}
]
# 2. 用字典建立新資料,並用 append 加進清單
new_item = input("請輸入品項: ")
new_price = int(input("請輸入金額: "))
new_cat = input("請輸入分類: ")
new_data = {
"name": ____,
"price": ____,
"category": ____
}
records.____(new_data) # 提示:加進清單的方法
# 3. 用 for 迴圈走訪累加總金額
total_sum = 0
for item in ____: # 提示:要走訪的清單名稱
print(f"• {item['name']}: ${item['price']} 元 [{item['category']}]")
total_sum += item[____] # 提示:要累加金額的字典 Key
print(f"💰 總花費金額:${total_sum} 元")
# 4. 條件過濾
target = input("請輸入欲過濾的分類: ")
for item in records:
if item["category"] ____ target: # 提示:比較相等的符號
print(f"🎯 找到符合項目:{item['name']}")
⏰ 單元 1:List 基礎與操作(09:00 - 10:00)
想像一下,如果每次買東西都要開一個新變數(price1, price2, price3...),帳本很快就會亂七八糟。清單(List)就像是一個「有順序的購物車口袋」,用語法 [] 建立。你可以使用 append() 把剛買的品項塞進口袋最後面,用 len() 算口袋裡有幾張發票,並用索引編號(如 items[0])拿取第一個商品。記得:**電腦數數是從 0 開始的喔!**
Python 的 List 是一種 **可變 (Mutable)** 且有序的序列。電腦在記憶體中為清單配置連續的指標空間:
- 0-Based Indexing:第一個元素的偏移量 (Offset) 為 0,因此
list[0]存取首筆資料。 - 負數索引
list[-1]:方便從清單尾端倒數存取最新加入的元素。 append()時間複雜度:平均為 $O(1)$,能高效率地在末端動態擴充資料。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請向初學者解釋 Python 的 List(清單)如何在記憶體中儲存資料,並說明 0-based index(從 0 開始的索引)與負數索引 (如 -1) 的運作原理。
- 單一變數像獨棟套房,清單像「多房間公寓」。
append()把資料推入末端;len()算長度;[0]存取首筆。- Django 銜接點:Django ORM 查詢產生的 QuerySet 集合。
# 1-1: 建立清單與存取索引
items = ["便當", "珍奶", "筆記本"]
print(f"第一個品項:{items[0]}") # 便當
print(f"清單總數量:{len(items)}") # 3
# 1-2: 動態加入新資料
items.append("運動飲料")
print("更新後的購物車:", items)
- 索引越界 (Out of Range):長度為 3 的清單存取
items[3]$\rightarrow$ 報錯IndexError: list index out of range。
我在用 Python 清單存資料時跳出了 IndexError: list index out of range 錯誤。請幫我分析原因,並說明 index 為什麼是從 0 到 len-1。
⏰ 單元 2:Dict 鍵值對(10:00 - 11:00)
如果只有清單,我們只知道買了 3 個東西,但不知道每樣東西的名字、價格與分類。字典(Dict)用語法 {} 建立,用 **「自訂標籤 (Key) : 內容 (Value)」** 來打包單一商品的完整細節。例如:{"name": "珍奶", "price": 60}。這樣只要查 ["price"],就能精準抓出這筆商品的金額!
Python 的 Dict 是透過 **Hash Table (雜湊表)** 實現的,具備極高的資料檢索效率:
- Key 的唯一性與不可變性:Key 必須是唯一且不可變的型別(如字串或數字)。
- 安全存取
dict.get('key', default):若 Key 不存在,使用[]會拋出KeyError,而.get()會優雅回傳None或預設值。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請向初學者說明什麼是 Python 字典(Dict)的 Key-Value 鍵值對,並示範使用 dict['key'] 與 dict.get('key') 存取資料時的差異與安全性。
- Dict 用自訂標籤 (Key) 找內容 (Value)。
- 語法:
{"key": "value"}。 - Django 銜接點:字典的 Key 等同於資料庫欄位(Database Field/Column)。
# 2-1: 宣告單筆消費字典
expense = {
"name": "珍珠奶茶",
"price": 60,
"category": "餐飲"
}
# 2-2: 存取與修改欄位
print(f"品項:{expense['name']}")
print(f"價格:${expense['price']} 元")
# 漲價更新
expense["price"] = 65
print(f"最新價格:${expense['price']} 元")
- 存取不存在的 Key:拼錯或存取未定義的 Key
expense["amount"]$\rightarrow$ 報錯KeyError: 'amount'。
我在存取 Python 字典時跳出了 KeyError 錯誤。請協助我檢查 Key 名稱是否拼錯,並教我如何使用 .get() 語法避免當機。
⏰ 單元 3:List + Dict 組合構建(11:00 - 12:00)
現在我們要結合前兩個單元的神技!把每一個商品的「字典標籤」塞進「清單購物車」裡,就變成了 **List of Dicts 二維資料結構**:records = [{...}, {...}]。這正是所有現代軟體與資料庫最核心的結構!清單代表「整張數據表 (Table)」,而裡面的每一個字典代表「一列消費紀錄 (Row)」。
組合 List 與 Dict 能建立強大的二維資料模型(Two-dimensional Data Structure):
- 雙重存取語法
records[0]["price"]:第一層[0]透過索引取出現第 1 筆字典,第二層["price"]取出金額。 - 對應 Excel / DB Table:Index 對應橫向列 (Row),Dict Key 對應縱向欄 (Column/Field)。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請用 Excel 表格的列(Row)與欄(Column)的概念,說明 Python List of Dicts 是如何對應真實資料庫表格結構的。
- 二維結構:
records = [{...}, {...}]。 - 雙重存取:
records[0]["price"]。 - Django 銜接點:資料庫表格(Database Table)。
# 3-1: 建立記帳數據庫雛形 (List of Dicts)
records = [
{"name": "珍珠奶茶", "price": 60},
{"name": "排骨便當", "price": 110}
]
# 3-2: 動態新增一筆紀錄
item_input = input("請輸入品項: ")
price_input = int(input("請輸入金額: "))
new_data = {"name": item_input, "price": price_input}
records.append(new_data)
print(f"目前第一筆名稱:{records[0]['name']}")
print(f"目前最後一筆金額:{records[-1]['price']}")
- 型別存取搞混:把 List 當作 Dict 直接寫
records["price"]$\rightarrow$ 報錯TypeError: list indices must be integers or slices, not str。
我在存取 List of Dicts 時遇到了 TypeError: list indices must be integers or slices, not str 錯誤。請教我如何用雙重存取語法 (如 list[0]['key']) 正確取出資料。
⏰ 單元 4:for 迴圈走訪與統計(13:00 - 14:00)
帳本裡如果有 100 筆資料,總不可能手動去加 `[0] + [1] + [2]` 吧?for 迴圈就像是一個「巡邏機器人」,語法 for item in records: 會自動掏出清單裡的每一個字典,幫我們印出細節,並透過累加變數 total += item["price"] 精確算出總花費與平均值!
走訪容器並計算統計數據是演算法的核心基礎:
- 初始化累加器
total = 0:必須放在迴圈外部,否則每次迭代都會被重置歸零。 enumerate(sequence, start=1):能同時取出當前項目的索引編號與內容物,非常適合用來印出排版明細。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請說明什麼是累加器模式(Accumulator Pattern),並示範如何用 for 迴圈搭配 enumerate 走訪 List of Dicts 算出總金額與平均數。
for item in records:逐筆取出字典。- 累加器變數
total_cost += item["price"]需宣告在迴圈外。 - Django 銜接點:Django HTML 模板中的
{% for item in items %}網頁清單渲染。
records = [
{"name": "珍珠奶茶", "price": 60},
{"name": "排骨便當", "price": 110},
{"name": "筆記本", "price": 45}
]
# 4-1: for 迴圈逐筆列印與累加
total_cost = 0
print("=== 消費明細 ===")
for item in records:
print(f"• {item['name']}: ${item['price']} 元")
total_cost += item["price"]
avg_cost = total_cost / len(records)
print(f"總計消費:${total_cost} 元")
print(f"平均花費:${avg_cost:.1f} 元")
- 累加器位置寫錯:把
total = 0寫在for迴圈內部,導致每次循環都被歸零。
我寫了 for 迴圈想要計算清單總金額,但算出來的總和永遠只有最後一筆金額。請幫我檢查累加器 (total) 宣告的位置是否放錯了。
⏰ 單元 5:專案實作:擴充資料模型(14:00 - 15:00)
專業軟體不會只紀錄名稱跟金額。我們現在要為資料模型擴充「分類(餐飲/學習/娛樂)」以及「自動日期時間戳記」。引入 Python 內建的 datetime.date.today(),讓系統在你新增消費時,自動幫你蓋上今天的日期印章!
在軟體工程中,統一資料欄位格式稱為定義 **Schema**:
datetime.date.today():獲取系統當前日期物件。.strftime("%Y-%m-%d"):將日期物件轉化為標準的字串格式(如 2026-08-03)。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請說明如何使用 Python datetime 模組取得今天的日期,並解說 .strftime('%Y-%m-%d') 中各字母格式代表的意義。
- 模型擴充:
category與date欄位。 - 使用
datetime.date.today().strftime("%Y-%m-%d")。 - Django 銜接點:Django Model 欄位擴充與 Migration 資料庫遷移。
import datetime
# 取得今天日期字串
today_str = datetime.date.today().strftime("%Y-%m-%d")
# 建立具備完整 Schema 的紀錄
record = {
"name": "Python 教科書",
"price": 550,
"category": "學習",
"date": today_str
}
print("擴充後的資料模型:", record)
- 舊資料缺欄位:舊的字典沒有
category欄位,讀取時噴出KeyError$\rightarrow$ 建立資料需統一 Schema。
我在擴充字典欄位後,存取舊資料時跳出了 KeyError。請教我如何在讀取欄位時提供預設值 (例如 .get('category', '一般'))。
⏰ 單元 6:小組黑客松:資料分類器(15:00 - 16:00)
記帳最終目的是要「檢討開銷」。如果我想知道「這個月到底喝了多少錢的珍奶?」,我們能在 for 迴圈裡面加上 if item["category"] == target: 判斷式。只有符合分類的項目才會被掏出來加總,並透過 .strip() 自動消除輸入時不小心多打的空格!這也是未來 Django 網頁後台一鍵搜尋資料庫的核心原理!
資料分析的核心步驟包括過濾與清洗:
.strip()字串清洗:能自動清除使用者輸入時頭尾多打的空白,避免"餐飲 " == "餐飲"比對失敗。- 對應 Django ORM:此邏輯等於 Django 資料庫查詢
Expenses.objects.filter(category='餐飲')。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請說明如何在 for 迴圈中用 if 條件式對 List of Dicts 進行欄位篩選,並解釋 .strip() 如何防止字串比較時因為多打空白而失敗。
for迴圈結合if item["category"] == target:比對。- 使用
.strip()消除頭尾白空白。 - Django 銜接點:Django ORM 語法
Expenses.objects.filter(category='餐飲')。
records = [
{"name": "珍珠奶茶", "price": 60, "category": "餐飲"},
{"name": "排骨便當", "price": 110, "category": "餐飲"},
{"name": "電影票", "price": 300, "category": "娛樂"}
]
target_cat = input("請輸入要搜尋的分類 (例如: 餐飲): ").strip()
cat_total = 0
print(f"\n--- 篩選分類:{target_cat} ---")
for item in records:
if item["category"] == target_cat:
print(f" • {item['name']}: ${item['price']} 元")
cat_total += item["price"]
print(f"分類小計:${cat_total} 元")
- 隱形空格比對失敗:輸入
"餐飲 "(帶有空白),導致"餐飲 " == "餐飲"結果永遠為False。
我在比對使用者輸入的分類名稱時,明明輸入了一模一樣的中文文字卻沒有觸發 if 條件。請幫我檢查字串是否有包含多餘的空白字元,並示範使用 .strip()。