昨天我們用 Dictionary 表示一筆待辦事項:
{
"id": 1,
"title": "買牛奶",
"priority": "高",
"done": False,
"created_at": "2026-09-24T21:30:15",
}
這樣其實能動,但寫久了會遇到三個問題:
todo["titel"] 執行到才發現 KeyError。今天要介紹的 Class(類別),就是用來解決這些問題的。
Class 是一個「模板」,描述某一類東西有什麼資料、能做什麼事。用這個模板產生出來的具體東西,叫做 **instance(實體)**或 object(物件)。
打個比方:
Class 是「待辦事項」這個概念——它有標題、有優先度、可以被完成instance 是「買牛奶」這張具體的便條紙
class Todo:
def __init__(self, title, priority="中"):
self.title = title
self.priority = priority
self.done = False
def complete(self):
self.done = True
# 建立兩個實體
t1 = Todo("買牛奶", "高")
t2 = Todo("整理房間", "低")
t1.complete()
print(t1.title, t1.done) # 買牛奶 True
print(t2.title, t2.done) # 整理房間 False
t1 和 t2 各自有獨立的資料,互不影響。
__init__ 與 self這兩個是初學者最容易卡住的地方,慢慢講。
__init__:建構子t1 = Todo("買牛奶", "高")
寫 Todo(...) 的時候,Python 會做兩件事:
__init__,把你給的參數傳進去所以 __init__ 的工作就是幫這個新物件設定初始狀態。名字前後各兩個底線,這是 Python 的特殊方法命名慣例(唸作 dunder init)。
self:這個物件自己self 代表「正在被操作的這個物件」。
def __init__(self, title, priority="中"):
self.title = title # 把參數 title 存到「這個物件」的 title 屬性上
注意左右兩邊是不同的東西:
self.title:這個物件的屬性(會一直存在)title:函式的參數(函式結束就消失)而呼叫方法的時候:
t1.complete()
Python 其實在背後做的是 Todo.complete(t1)——它自動把 t1 當成第一個參數傳進去。所以定義方法時第一個參數一定要寫 self。
我剛學的時候一直覺得
self很多餘,後來才理解:它讓「同一段程式碼」可以作用在「不同的物件」上。t1.complete()和t2.complete()跑的是同一段程式碼,但改的是不同物件的資料。
把待辦事項做得實用一點:
from datetime import datetime
class Todo:
"""一筆待辦事項。"""
VALID_PRIORITIES = ("高", "中", "低")
def __init__(self, title, priority="中", done=False, created_at=None):
if not title.strip():
raise ValueError("標題不能是空的")
if priority not in self.VALID_PRIORITIES:
raise ValueError(f"優先度只能是 {'/'.join(self.VALID_PRIORITIES)}")
self.title = title.strip()
self.priority = priority
self.done = done
self.created_at = created_at or datetime.now().isoformat(timespec="seconds")
def complete(self):
"""標記為完成。"""
self.done = True
return self
def is_urgent(self):
"""是否為高優先度且尚未完成。"""
return self.priority == "高" and not self.done
def to_dict(self):
"""轉成 dict,方便存成 JSON。"""
return {
"title": self.title,
"priority": self.priority,
"done": self.done,
"created_at": self.created_at,
}
@classmethod
def from_dict(cls, data):
"""從 dict 建回物件。"""
return cls(
title=data["title"],
priority=data.get("priority", "中"),
done=data.get("done", False),
created_at=data.get("created_at"),
)
def __repr__(self):
mark = "✓" if self.done else " "
return f"<Todo [{mark}] {self.title}({self.priority})>"
幾個新東西:
VALID_PRIORITIES(類別屬性)
寫在 class 裡面、方法外面的變數,是所有實體共用的。適合放常數。
__repr__
定義「這個物件印出來長什麼樣」。沒有定義的話,print(t1) 會印出 <__main__.Todo object at 0x000001A2...>,完全看不出是什麼。加上 __repr__ 之後:
print(Todo("買牛奶", "高"))
# <Todo [ ] 買牛奶(高)>
除錯的時候會非常感謝自己。
@classmethod 與 from_dict
這是一個「替代建構子」。Todo(...) 是從參數建立物件,Todo.from_dict(...) 是從一個 dict 建立物件。cls 代表這個 class 本身,所以 cls(...) 等同於 Todo(...)。
to_dict / from_dict 這一對
這組方法是關鍵——它讓 Class 和昨天學的 JSON 接得起來:
import json
todos = [Todo("買牛奶", "高"), Todo("整理房間", "低")]
todos[0].complete()
# 存檔
with open("todos.json", "w", encoding="utf-8") as f:
json.dump([t.to_dict() for t in todos], f, ensure_ascii=False, indent=2)
# 讀檔
with open("todos.json", "r", encoding="utf-8") as f:
loaded = [Todo.from_dict(d) for d in json.load(f)]
print(loaded)
# [<Todo [✓] 買牛奶(高)>, <Todo [ ] 整理房間(低)>]
**記憶體裡用物件(方便操作),存檔時轉成 JSON(方便保存)。**這個模式在後面的專案裡會一直出現。
如果你的 class 主要是在裝資料,Python 有一個省事的寫法:
from dataclasses import dataclass, field, asdict
from datetime import datetime
@dataclass
class Todo:
title: str
priority: str = "中"
done: bool = False
created_at: str = field(
default_factory=lambda: datetime.now().isoformat(timespec="seconds")
)
def complete(self):
self.done = True
return self
t = Todo("買牛奶", "高")
print(t)
# Todo(title='買牛奶', priority='高', done=False, created_at='2026-09-25T09:12:03')
print(asdict(t)) # 直接轉成 dict,不用自己寫 to_dict
@dataclass 自動幫你產生 __init__、__repr__、__eq__,程式碼少一半。
⚠️ 注意 created_at 那行用的是 field(default_factory=...) 而不是直接 = datetime.now()...。因為預設值只會在定義 class 的時候算一次,所有物件會共用同一個時間。default_factory 才會在每次建立物件時重新執行。這是很經典的坑。
有時候我們會有「一般的 X」和「特殊的 X」:
class Tool:
"""所有工具的基底類別。"""
name = "unnamed"
description = "沒有說明"
def run(self, **kwargs):
raise NotImplementedError("子類別必須實作 run()")
def to_schema(self):
"""轉成可以給 AI 看的工具定義。"""
return {"name": self.name, "description": self.description}
class WeatherTool(Tool):
name = "get_weather"
description = "查詢指定城市的今日天氣"
def run(self, city):
return f"{city} 今天晴天,氣溫 26 度"
class TodoTool(Tool):
name = "add_todo"
description = "新增一筆待辦事項"
def __init__(self, store):
self.store = store
def run(self, title, priority="中"):
todo = Todo(title, priority)
self.store.append(todo)
return f"已新增待辦:{title}"
# 用法
store = []
tools = [WeatherTool(), TodoTool(store)]
for tool in tools:
print(tool.to_schema())
print(tools[0].run(city="台北"))
print(tools[1].run(title="買牛奶", priority="高"))
print(store)
輸出:
{'name': 'get_weather', 'description': '查詢指定城市的今日天氣'}
{'name': 'add_todo', 'description': '新增一筆待辦事項'}
台北 今天晴天,氣溫 26 度
已新增待辦:買牛奶
[<Todo [ ] 買牛奶(高)>]
class WeatherTool(Tool) 表示 WeatherTool 繼承自 Tool。它自動擁有 to_schema(),不用重寫。
raise NotImplementedError 是一個約定:父類別定義「必須有這個方法」,但不實作它,強迫子類別自己寫。
注意最後那個迴圈——tools 裡面裝著兩種不同的工具,但我們可以用同一種方式對待它們。這叫多型(polymorphism),而它正是 Agent 工具系統的核心:
Agent 不需要知道每個工具的細節,它只需要知道「每個工具都有 name、description,而且都能 run()」。
物件導向不是必須的——你完全可以用 dict 和 function 寫出一個能動的 Agent。但當專案長大之後,Class 會在這幾個地方幫上忙:
1. 工具(Tool)
剛剛那個 Tool 基底類別就是雛形。Day 22 我們會把它擴充成完整的工具註冊系統。
2. 訊息與對話(Message / Conversation)
一輪對話包含 role(user / assistant)、content、時間戳記、可能還有工具呼叫紀錄。用 Class 包起來比一堆巢狀 dict 清楚太多。
3. Agent 本身
class Assistant:
def __init__(self, name, tools, memory):
self.name = name
self.tools = tools
self.memory = memory
self.history = []
def chat(self, user_input):
...
Agent 需要記住工具清單、記憶、對話歷史——這些是狀態,而 Class 就是拿來裝狀態的。
4. 記憶(Memory)
class Memory:
def remember(self, key, value): ...
def recall(self, key): ...
def forget(self, key): ...
把記憶的「介面」定義好,實作可以是檔案、資料庫、向量資料庫,換掉實作不影響其他程式碼。
不是所有東西都要寫成 Class。我自己的判斷是:
| 情況 | 建議 |
|---|---|
| 單純一組不會變的資料 | dict 或 dataclass 就夠 |
| 資料有行為(可以對它做事) | Class |
| 需要維護狀態(會一直變) | Class |
| 多個東西共用同一套介面 | Class + 繼承 |
| 只是一個單純的轉換動作 | Function 就好(回想 Day 7) |
**不要為了用 Class 而用 Class。**一個只有 __init__ 的 class,跟一個 dict 沒什麼差別,那就用 dataclass 或 dict。
__init__ 負責初始化,self 代表「這個物件自己」__repr__ 讓物件印出來人看得懂,除錯必備to_dict() / from_dict() 這一對讓物件跟 JSON 之間可以來回轉換@dataclass 幫你省掉大量樣板程式碼,但小心 default_factory 的坑到今天為止,Python 的核心語法基礎就告一段落了。從明天開始,我們要讓程式走出去——接觸外面的世界。
明天先處理一件事前準備:環境變數與 API Key 的安全管理。在開始串 API 之前,我們得先知道怎麼把密鑰藏好。