iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
AI Engineering

從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理系列 第 11 篇

用 Class 描述一件事:物件導向與 Agent 的角色設計

  • 分享至 

  • xImage
  •  

昨天我們用 Dictionary 表示一筆待辦事項:

{
    "id": 1,
    "title": "買牛奶",
    "priority": "高",
    "done": False,
    "created_at": "2026-09-24T21:30:15",
}

這樣其實能動,但寫久了會遇到三個問題:

  1. 打錯字沒人提醒:todo["titel"] 執行到才發現 KeyError。
  2. 不知道有哪些欄位:翻遍整份程式碼才知道這個 dict 應該長什麼樣。
  3. 行為跟資料分家:「把這筆標記完成」這個動作,寫在別的地方,跟資料本身沒有關聯。

今天要介紹的 Class(類別),就是用來解決這些問題的。


一、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 會做兩件事:

  1. 建立一個空的 Todo 物件
  2. 自動呼叫 __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() 跑的是同一段程式碼,但改的是不同物件的資料。


三、讓 Class 更完整

把待辦事項做得實用一點:

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(方便保存)。**這個模式在後面的專案裡會一直出現。


四、dataclass:少寫一點樣板

如果你的 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 才會在每次建立物件時重新執行。這是很經典的坑。


五、繼承:讓一個 Class 基於另一個

有時候我們會有「一般的 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()」。


六、Class 在 Agent 架構裡的位置

物件導向不是必須的——你完全可以用 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?

不是所有東西都要寫成 Class。我自己的判斷是:

情況 建議
單純一組不會變的資料 dict 或 dataclass 就夠
資料有行為(可以對它做事) Class
需要維護狀態(會一直變) Class
多個東西共用同一套介面 Class + 繼承
只是一個單純的轉換動作 Function 就好(回想 Day 7)

**不要為了用 Class 而用 Class。**一個只有 __init__ 的 class,跟一個 dict 沒什麼差別,那就用 dataclass 或 dict。


小結

  • Class 是模板,instance 是用模板做出來的具體物件
  • __init__ 負責初始化,self 代表「這個物件自己」
  • __repr__ 讓物件印出來人看得懂,除錯必備
  • to_dict() / from_dict() 這一對讓物件跟 JSON 之間可以來回轉換
  • @dataclass 幫你省掉大量樣板程式碼,但小心 default_factory 的坑
  • 繼承 + 多型 讓 Agent 可以用統一的方式對待各種不同的工具

到今天為止,Python 的核心語法基礎就告一段落了。從明天開始,我們要讓程式走出去——接觸外面的世界。

明天先處理一件事前準備:環境變數與 API Key 的安全管理。在開始串 API 之前,我們得先知道怎麼把密鑰藏好。


上一篇
讓資料能被交換:JSON 與 Agent 的共通語言
下一篇
把鑰匙藏好:環境變數、.env 與 API Key 管理
系列文
從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言