Python OOP 互動實驗室 · 從痛點開始的學習路徑

為什麼會有物件導向?
先讓你感覺一次 沒有它的痛

多數教學上來就講 class__init__self,看完還是一頭霧水——那是因為你沒搞懂「為何要有 OOP」。這份教材反過來:先讓你寫沒有 OOP 的程式,痛一次,再讓 OOP 出來救你。用 iPhone 當主角,貫穿到底。

01沒有 OOP 的痛 02OOP 的核心想法 03核心詞彙 04iPhone 比喻 05拆解 class iPhone 06Before vs After 07OOP 的代價 08__init__ 慢動作 09iPhone 工廠遊戲 10記憶體實驗室 11替身工坊(彩蛋) 12挑戰
01 · 先把痛感放出來

在 OOP 出現以前,
大家是這樣 勉強寫 的。

OOP 不是憑空冒出來的,它是為了解決真實的痛點而生。要懂 OOP 的價值,最快的方法是先看一段沒有它的程式碼——當你看完覺得「這也太慘」,OOP 對你就會像救命稻草。

📱

情境設定

你要寫一個程式,管理辦公室裡 3 支 iPhone:Akira 的、Mei 的、Tom 的。每支手機都有「型號、顏色、電量、擁有者」,並且要能「打電話、拍照、查電量」。聽起來很簡單對吧?但沒有 OOP 的世界,這事情會很噁心。

C 語言 痛感 70%

用 C 寫,最原始的做法

struct 把資料湊在一起,但每個函式都要手動傳入是哪一支手機。資料和操作完全分家,函式飄在 struct 外面,誰都管不到誰。

struct iPhone { char model[20]; int battery; char owner[30]; }; // 函式和 struct 沒有綁定, // 每次都要手動傳「哪支手機」 void make_call(struct iPhone *p, char *num) { if (p->battery < 5) return; p->battery -= 1; printf("%s→%s\n", p->owner, num); } int main() { // 每支都要手動填欄位 struct iPhone phone1; strcpy(phone1.model, "iPhone 15"); strcpy(phone1.owner, "Akira"); phone1.battery = 80; struct iPhone phone2; // ... 又重複 3 行 ... // 呼叫一定要指定哪支 make_call(&phone1, "0912"); make_call(&phone2, "0987"); return 0; }
Python · 純變數 痛感 80%

換 Python,但不用 class

變數爆炸:每支手機 6 個變數,3 支就 18 個。函式參數一大堆,回傳值還要存回對的變數——一不小心就傳成 phone1_ownerphone2_battery,bug 找不到。

# 每支手機 6 個變數 phone1_model = "iPhone 15" phone1_battery = 80 phone1_owner = "Akira" phone2_model = "iPhone 14" phone2_battery = 100 phone2_owner = "Mei" phone3_model = "iPhone 13" phone3_battery = 50 phone3_owner = "Tom" # 想加新屬性?所有 phoneN_* # 都要動,改一處改三處 def make_call(owner, battery, num): if battery < 5: return battery, "電量不足" return battery - 1, \ f"{owner}{num}" # 呼叫超痛苦: phone1_battery, msg = make_call( phone1_owner, phone1_battery, "0912" ) # 傳錯參數順序 → 死人
Python · dict 改良 崩潰 95%

改用 dict?還是不夠

表面上整齊了一點,但更深的問題藏不住:字串 key 打錯不報錯、沒人保證每個 dict 都有相同 key、函式還是飄在外面沒有歸屬。核心病根:資料和能對它做的事,根本沒綁在一起

# 改用 dict 看起來好點 phones = [ {"model": "iPhone 15", "battery": 80, "owner": "Akira"}, {"model": "iPhone 14", "battery": 100, "owner": "Mei"}, ] def make_call(p, num): if p["battery"] < 5: return "電量不足" p["battery"] -= 1 return f"{p['owner']}{num}" # ❌ key 打錯不報錯 # p["batery"] → None # ❌ 沒人保證 dict 齊全 # ❌ 函式飄在外面 # ❌ 新增功能 → 越來越亂 # 核心病根: # 「資料」和「能做的事」 # 沒有綁在一起。

變數命名地獄

phone1_batteryphone2_battery... 加到第 50 支時你會想離職

函式參數爆炸

每個函式都要傳一大堆參數,順序錯一個就完蛋,IDE 也救不了你

資料和操作分家

手機資料在這、能對它做的事在那,沒人告訴你「手機這個東西能做什麼」

修改一處改十處

多加一個屬性,所有 phoneN_* 變數都要新增,所有相關函式都要改

02 · 主角登場

於是有人想到:
「把資料操作綁在一起就好了啊」

這就是物件導向(Object-Oriented Programming,OOP)誕生的核心動機。1960 年代的 Simula 67、1970 年代的 Smalltalk,到後來的 C++、Java、Python——都是在實踐同一個簡單想法:把屬於同一個東西的「資料」和「行為」打包在一起,當作一個整體來處理。

The Big Idea

「手機的型號、電量、擁有者,
打電話、拍照、查電量
本來就是同一件事的兩面。」

既然如此,何不寫一個叫 iPhone 的「類別」,把這些東西全部裝進去?以後想要一支新手機,就用這個類別「造」一個出來。它自帶屬性、自帶行為——你不用再記「哪個變數要配哪個函式」。

封裝(Encapsulation)

把屬於一起的東西包成一包。手機的所有資料 + 行為,都在 iPhone 類別裡。

抽象(Abstraction)

外面的人不用知道內部怎麼運作,只要會用「方法」就好。phone.make_call() 就這麼簡單。

可重用(Reusable)

寫一次 iPhone 類別,可以造出無限支實例。新增一支不再需要複製 6 行變數。

請先記住這句話:「Class 是把屬性(attribute)方法(method)綁在一起的設計圖,instance(實例)是依設計圖造出來的個體。」剩下的全部都是這句話的延伸。
03 · 兩組核心詞彙

在繼續之前,先搞清楚
「屬性 vs 方法」「方法 vs 函式」

這兩組詞會貫穿整份教材。如果搞混了,後面看程式碼會覺得每個字都認識、整句卻不知道在說什麼。花 3 分鐘把它釐清,後面就一路通暢。

屬性(attribute) vs 方法(method)

類別裡的東西,基本上只分這兩種:

📋
屬性 attribute
= 物件的「資料」
這個物件「是什麼樣子」、「目前狀態如何」。用名詞描述。在程式碼中以 self.xxx 形式存在。
iPhone 的屬性
  • self.model = "iPhone 15"
  • self.color = "黑色"
  • self.battery = 80
  • self.owner = "Akira"
方法 method
= 物件的「行為」
這個物件「能做什麼」、「會怎樣動」。用動詞描述。在 class 裡用 def 定義,第一個參數是 self。
iPhone 的方法
  • make_call() — 打電話
  • take_photo() — 拍照
  • charge() — 充電
  • install_app() — 安裝 App
💡 記憶口訣:名詞是屬性,動詞是方法。手機「」什麼(顏色、電量)→ 屬性;手機「」什麼(打電話、拍照)→ 方法。所以一個 class 寫好後,你可以對它說:「告訴我你什麼資料、你做哪些事」——這兩個問題的答案,就是這個 class 的全部。

方法(method) vs 函式(function)

很多人一直搞混這兩個字——它們長得幾乎一樣,但身分完全不同:

🔨
函式 function
= 獨立的工具,誰都能用
寫在 class 外面不屬於任何物件。誰都可以直接呼叫它。第一個參數不需要 self。
# 寫在 class 外,是獨立函式 def add(a, b): return a + b # 直接呼叫,不依附任何物件 result = add(3, 5) # Python 內建函式也是這種 print("hi") len("abc") max(1, 2, 3)
🔧
方法 method
= 物件專屬的技能
寫在 class 裡面,屬於某個物件。必須透過物件呼叫,前面要加「物件名 + 點」。第一個參數一定是 self。
# 寫在 class 裡,是 iPhone 的方法 class iPhone: def make_call(self, num): ... # 必須透過實例(物件)呼叫 akira_phone.make_call("0912") # Python 內建型別也有方法 "hi".upper() [1,2].append(3)
💡 一句話分辨:看呼叫方式就知道。function(...) 是函式;obj.method(...)(前面有一個物件加點號)就是方法。

例:len("abc")函式(直接呼叫);"abc".upper()方法("abc" 這個字串物件呼叫自己的 upper 方法)。本質上,方法就是「綁在物件身上」的函式——它知道是「誰」在呼叫自己(透過 self)。
04 · 用 iPhone 認識

想像你站在 Apple HQ

Apple 總部裡有一張設計圖(class),上面寫著 iPhone 是什麼、能做什麼。富士康工廠看著這張設計圖,造出無數支實際的 iPhone(instances)。每一支都有相同的能力(打電話、拍照),但每一支的狀態都不一樣(不同擁有者、不同電量、不同顏色)。

📐
設計圖
class iPhone:
🏭
工廠生產
iPhone(...)
📱
Akira
📷
Mei
🎵
Tom
3 個獨立的實例

類別 Class · 設計圖

定義「iPhone 這個東西長什麼樣、能做什麼」。寫 class iPhone: 還沒造出任何手機,只是定義規格。

📐 在 Apple HQ 安靜地躺著,只有一份

實例 Instance · 真實手機

依設計圖造出來的具體個體。Akira 的、Mei 的、Tom 的手機都是 iPhone 的 instance。寫 p = iPhone(...) 才真正造出一支。

📱 全世界有 無數支,每支獨立。

🍎 關鍵差別:整個 Apple 公司只有「一份 iPhone 設計圖」,但全球賣出了上億支「iPhone 實例」。設計圖不會被「用掉」,它一直在 Apple 內部;每支實際手機是依設計圖造出來的獨立個體賣給你的是手機本體(實例),不是設計圖(類別)
05 · 拆解第一個類別

把「iPhone 設計圖」翻譯成 Python。

下面這段是「iPhone 設計圖」的真實 Python 程式碼。點任何一行,會跳出該行在做什麼、為什麼這樣寫。常見元件:class__init__selfattributemethod

1
class iPhone:
2
    # 一、初始化:每支手機剛被造出來時要做的事
3
    def __init__(self, model, color, owner, battery=100):
4
        self.model = model
5
        self.color = color
6
        self.owner = owner
7
        self.battery = battery  # 出廠預設 100%
8
 
9
    # 二、方法:手機會做的事
10
    def make_call(self, number):
11
        if self.battery < 5:
12
            return f"{self.owner} 電量不足,不能打"
13
        self.battery -= 1
14
        return f"{self.owner}{self.model} 打給 {number}"
15
 
16
    def charge(self, minutes):
17
        self.battery = min(100, self.battery + minutes)
18
 
19
# === 使用:用同一張設計圖造出 3 支獨立手機 ===
20
akira_phone = iPhone("iPhone 15", "黑色", "Akira", 80)
21
mei_phone = iPhone("iPhone 14", "藍色", "Mei")
22
akira_phone.make_call("0912")  # 只影響 Akira 的手機
點上面任何一行看說明 ↑
這份是完整的 iPhone 類別。包含一個 __init__(出廠時要做的事)和兩個方法(make_callcharge)。第 20-22 行示範怎麼使用——同一張設計圖,造出兩支獨立的手機。
06 · 並排對照

同一個任務,
沒 OOP vs 有 OOP

光講理論沒用,直接看程式碼最有感。下面這個任務在左右兩種寫法下會長什麼樣,請仔細看,特別注意:呼叫的時候有多麻煩、加新手機要動多少地方、整段程式碼有沒有「歸屬感」。

📋 共同任務
建立 3 支手機,幫 Akira 那支打通電話,然後幫 Mei 那支充 30 分鐘電。

❌ 沒有 OOP

# procedural.py · 47 行
# 1. 用變數存所有屬性 phone1_model = "iPhone 15" phone1_color = "黑色" phone1_owner = "Akira" phone1_battery = 80 phone2_model = "iPhone 14" phone2_color = "藍色" phone2_owner = "Mei" phone2_battery = 100 phone3_model = "iPhone 13" phone3_color = "白色" phone3_owner = "Tom" phone3_battery = 50 # 2. 函式要傳一堆參數 def make_call(owner, battery, number): if battery < 5: return battery, "電量不足" return battery - 1, f"{owner}{number}" def charge(battery, minutes): return min(100, battery + minutes) # 3. 呼叫超痛苦 phone1_battery, msg = make_call( phone1_owner, phone1_battery, "0912" ) phone2_battery = charge(phone2_battery, 30) # 想加第 4 支?再寫 4 個變數 # 想加新屬性?所有 phoneN_xxx 都要動

✅ 用 OOP

# oop.py · 23 行
# 1. 寫一次設計圖(class) class iPhone: def __init__(self, model, color, owner, battery): self.model = model self.color = color self.owner = owner self.battery = battery def make_call(self, number): if self.battery < 5: return "電量不足" self.battery -= 1 return f"{self.owner}{number}" def charge(self, minutes): self.battery = min(100, self.battery + minutes) # 2. 造 3 支手機,乾淨清爽 p1 = iPhone("iPhone 15", "黑色", "Akira", 80) p2 = iPhone("iPhone 14", "藍色", "Mei", 100) p3 = iPhone("iPhone 13", "白色", "Tom", 50) # 3. 呼叫超直覺:誰要做什麼,就讓誰做 p1.make_call("0912") p2.charge(30) # 加第 4 支?一行解決 # 加新屬性?只改 __init__ 一個地方
❌ 沒 OOP 的代價
  • 每支手機 4 個變數,3 支就 12 個
  • 呼叫要傳 3+ 個參數,回傳值要解構再存回
  • 函式和資料分家,找不到「手機能做什麼」
  • 加屬性要改 N 處(每支手機都要動)
✅ 有 OOP 的甜頭
  • 設計圖寫一次,造 3 支只要 3 行
  • 呼叫直覺:p1.make_call("0912"),p1 自己知道自己的電量
  • 方法和屬性綁在類別裡,iPhone. 一打 IDE 就提示所有功能
  • 加屬性只改 __init__ 一處,全部實例自動更新規格
🎯 關鍵差別:OOP 不是讓程式變短(雖然通常會),而是讓程式變得能擴張。3 支手機可能還沒感覺,但 30 支、300 支、3000 支時,沒有 OOP 的版本會壓垮你,OOP 版本還是同一個 class、同一個用法。
07 · 平衡一下觀點

老實說,OOP 不是萬靈丹

看完前面,你可能想:「太棒了,從今天起所有程式都要用 OOP!」慢著。OOP 是為了解決「複雜系統」的問題,但複雜系統的工具用在簡單事情上,反而是負擔。誠實面對 OOP 的代價,才是真的學會它——也才知道什麼時候不要用它。

🚽

核心比喻:你只是想上個廁所...

假設你只想上個廁所,但你堅持要先蓋一棟房子——挖地基、砌牆、裝水電、領使用執照、繳房屋稅——才肯使用裡面的廁所。光想就崩潰對吧?這就是「用 OOP 寫一個只算 2+3 的小工具」會發生的事。

蓋房子是好事,但看你要做什麼。「住一輩子」要蓋房子,「上個廁所」找個流動廁所就好。OOP 是「蓋房子」這個能力——複雜、強大、可以擴張、未來增建容易,但不是每件小事都該用它

過度設計的真實例子

同樣是「算個三角形面積」這種簡單任務,硬要用 OOP 會變成什麼樣:

❌ OOP 蓋房子版

# 8 行,需要先理解 class、self、實例化
# 為了算三角形面積,建一個 class class TriangleCalculator: def __init__(self, base, height): self.base = base self.height = height def calc(self): return self.base * self.height / 2 # 使用 t = TriangleCalculator(3, 4) area = t.calc() # 結果:6.0,但你多寫了 7 行

✅ 流動廁所版

# 2 行,直接看就懂
# 一個函式就夠了 def triangle_area(base, height): return base * height / 2 # 使用 area = triangle_area(3, 4) # 結果:6.0,乾淨俐落

OOP 的四個代價

真實的 OOP 不只有好處,這四件事你應該知道:

01

過度設計(over-engineering)

本來 3 行就解決的事,硬包成 class 會變 30 行。簡單工具寫成 OOP,反而難讀、難改。剛學 OOP 的人最容易犯這個錯——什麼都用 class 包起來,整份程式變成一棟棟空房子。

02

記憶體與效能成本

每個實例都會在記憶體開一塊空間存自己的屬性。一萬個實例 = 一萬份屬性。方法呼叫也比直接呼叫函式多一層查詢開銷(在 Python 微小、在 C++ / 嵌入式 / 即時系統可能很重要)。對效能敏感的場景要慎用

03

學習與心智成本

OOP 有一整套詞彙:class、instance、self、attribute、method、繼承、多型、封裝……。同樣是「印出 hello」,OOP 版本要先看懂這些觀念才能讀懂。新手友善度低於 procedural——所以函式式入門通常比 OOP 入門快。

04

不適合「無狀態」的純運算

OOP 的強項是「物件帶著狀態走」。但純粹的數學運算、資料轉換(map、filter、reduce、單純的 if/else)用函數式風格更乾淨。Python 支援 OOP 但也支援函數式,不是非用 class 不可——很多工具用函式寫反而簡潔得多。

該不該用 OOP?問自己這 3 題

  1. 我會建立多個同類型的東西嗎(多支手機、多個玩家、多筆訂單、多個使用者)?
  2. 這些東西有自己的狀態會隨時間變化嗎(電量、生命值、餘額、登入狀態)?
  3. 這些東西有對應的行為嗎(打電話、攻擊、提款、登出)?
3 題都「是」 → 大膽用 OOP,這是它的舞台。
有 1–2 題「否」 → 想想是不是用函式 + dict 更清爽。
3 題都「否」 → 把 class 收起來,寫個函式就好。別蓋房子去上廁所。
08 · 慢動作回放

當你寫 akira_phone = iPhone(...)
Python 究竟做了什麼?

這是大多數初學者卡關的地方。__init__ 不是你呼叫的,是 Python 偷偷幫你呼叫;self 也不是你傳的,是 Python 偷偷塞進去。把整個過程拆成 5 個步驟,慢動作看一遍,就不再神秘。看懂這節,後面玩工廠遊戲時你會清楚每按一次按鈕背後究竟發生什麼事。

① 你寫的這行
② Python 開新房間
③ 偷塞 self
④ 跑 __init__
⑤ 還給你變數
👻
⚠️ 容易被忽略的關鍵

沒有名字的實例 = 鬼魂手機

上面 step ⑤ 講到「Python 把新物件交回給你」——這個「交回」要被接住才有意義。記憶體就像 Apple 的倉庫,空間有限。每支造出來的手機,都需要一個「名字」(變數名)指向它的記憶體位置,倉庫管理員才知道這支屬於誰、要保留下來。

❌ 沒接住的鬼魂手機
iPhone("iPhone 15", "黑色", "Akira", 80) # 手機造出來了,但沒人指向它 # 沒有名字 → 你之後找不到它 # Python 馬上回收這塊記憶體 # 等於:白做工 ☠️
✅ 有名字才存在
akira_phone = iPhone("iPhone 15", "黑色", "Akira", 80) # akira_phone 指向這支手機 # Python 知道「有人在用它」 # 不會被回收 # 之後 akira_phone.make_call(...) 隨意用
💡 關鍵觀念:記憶體就像旅館房間,旅館不會永遠留著沒人入住的房間。變數名 = 入住登記——只要還有變數指向這個物件,它就保留;當沒有任何變數指向它的瞬間,Python 的垃圾回收(garbage collection)就會把它清掉、把那塊記憶體釋放給其他物件用。
📌 所以建立實例幾乎都要寫成 變數 = ClassName(...)這個變數既是這個物件的「名字」,也是它存活的證明。少了它,你剛蓋好的手機就是一隻看不見、馬上消失的鬼魂。
⚙️ 進階小細節:同一個變數改指向,舊物件也會消失

變數不是物件本身,而是「指向物件的標籤」。同一個變數可以「改貼到」其他物件上:

phone = iPhone("iPhone 15", ...) # phone 指向手機 A phone = iPhone("iPhone 14", ...) # phone 改指向手機 B # 手機 A 沒有任何變數指向它了 → 也會被回收

所以「同一個變數名重複使用」會讓前一個物件失去 reference 而消失。要同時保留多支手機,就要用不同的變數名(akira_phone、mei_phone、tom_phone...),或用 list / dict 一起裝起來。

09 · 互動遊戲一

iPhone 工廠:你來填規格,Python 來造手機。

把剛剛學的全部串起來。你填的每個欄位就是傳給 __init__ 的「參數」。按下「製造」,左邊的程式碼會即時生成,右邊倉庫會多一支獨立的手機——每支都是同一個類別的不同實例。試試造 5 支看看記憶體裡的樣子。

🏭

iPhone Factory

填寫規格表 → 呼叫 iPhone 類別 → 產生實例

新手機規格表

📜 等同於執行這段 Python:
class iPhone: def __init__(self, model, color, storage, owner, battery): self.model = model self.color = color self.storage = storage self.owner = owner self.battery = battery # 還沒造任何手機,點按鈕試試 →
📦 倉庫(記憶體中的所有實例) 0
倉庫是空的,按左邊製造一支 👈
10 · 互動遊戲二

記憶體實驗室:每支 iPhone 都是 獨立個體

初學者最大的誤解:「同一個類別的實例,會不會互相影響?」不會。每個實例都有自己的記憶體空間。下面有 3 支同樣由 iPhone 類別造出來的手機,去操作它們——你會發現它們各玩各的,互不干涉。每個按鈕都是呼叫一個「方法」。

🧪 三支獨立 iPhone · 各自獨立的方法呼叫

所有操作都是「phone.method()」的形式,方法只會影響呼叫它的那支手機。執行紀錄會顯示 Python 實際做的事。

[等待操作... 試試按上面的方法按鈕]
💡 看仔細:當你點 akira_phone.charge(20),只有 Akira 的電量增加,Mei 和 Tom 的完全沒變化。每支手機的 self 都指向自己——這就是「實例獨立」的真正意義。如果是純變數版本,你要記住「呼叫 charge 時要傳 phone1 的 battery」,OOP 版本:「phone.charge(),它自己知道自己是誰」。
11 · 互動遊戲三 · 隱藏關卡

替身工坊 · Stand Builder

最後給你一個彩蛋:把學到的 OOP 思維套到完全不同的情境——建立 Stand 類別產生你自己的替身。一樣的概念(class、instance、__init__、attribute),不同的故事。這就是 OOP 的威力:一套思維可以套用在任何東西上——iPhone、替身、員工、訂單、銀行帳戶、遊戲角色,全部都是 class + instance。

「我,要建立一個替身。」

📖 同一個 Stand 類別,可以產出無數個獨立的替身實例。試試把 Star Platinum 改成你自己的 Stand。

Star Platinum
極速精密的近距離戰鬥
Power
95
Speed
98
Precision
90
Range
15
class Stand: def __init__(self, name, ability, power, speed, precision, range_): self.name = name self.ability = ability self.power = power self.speed = speed self.precision = precision self.range = range_ my_stand = Stand("Star Platinum", "極速精密的近距離戰鬥", 95, 98, 90, 15)
12 · 驗收

10 題挑戰,看看你是真懂還是假懂

混合「為什麼有 OOP」、「class vs instance」、「self 是什麼」、「method vs function」、「實例獨立性」、「何時不該用 OOP」、「跨情境應用」。答錯會立刻解釋。重點不是分數,是有沒有真的看懂每題的觀念。