🗺️ 程式互動式地圖
零基礎起步・第 14 課

類別與物件入門

self 到底是誰?為什麼定義方法要寫 self,呼叫時卻不用傳?明明是兩個名字,改了一個另一個也跟著變?這一課一步一步看 Python 照設計圖做出物件,把這些疑問一次弄清楚。

設計圖與照圖做出的物件 __init__ 與 self 同一杯,還是兩杯

💡 先搞懂問題

第 9 課幫虛構的「晴空咖啡」寫過算錢的函式。現在店長想把整個點單流程寫成程式:每一杯飲料都有品名、尺寸、加料三項資料,也有「加料」「算價錢」這些動作。用前面學過的東西來寫,最直覺的做法是每一杯取一組名字:name1、size1、tops1 是第一杯,name2、size2、tops2 是第二杯,再寫一個函式 add_topping(tops, item) 負責加料。

兩杯還應付得來,十杯就亂了。同一杯的三項資料,只靠名字結尾的數字湊在一起;加料時要自己記得把「這一杯」的清單傳進函式,傳錯了,珍珠就加到別人的飲料上,而且 Python 完全不會提醒你,因為對它來說那只是一個普通的串列。第 6 課的字典可以把一杯的資料收在一起,可是「加料」「算價錢」這些動作仍然是另外寫的函式,資料和動作還是分開的。

沒有類別:一杯的資料散在好幾個名字裡 name1 = "拿鐵"; size1 = "M" tops1 = [] name2 = "紅茶"; size2 = "L" tops2 = [] # 想替紅茶加珍珠…… add_topping(tops1, "珍珠") 同一杯的三項資料,只靠名字 結尾的數字 1、2 湊在一起 最後一行傳成 tops1, 珍珠加到了拿鐵上 Python 不會出現任何錯誤訊息 有類別:資料和動作包在同一個物件裡 a = Drink("拿鐵", "M") b = Drink("紅茶", "L") b.add("珍珠") → 只會加在 b 這一杯 類別 class(設計圖,一份) Drink 方法:__init__、add 照圖做出 Drink 物件(a 貼著) .name = '拿鐵' .size = 'M' .toppings = [] Drink 物件(b 貼著) .name = '紅茶' .size = 'L' .toppings = ['珍珠']
上半是只用變數和函式的寫法:哪些名字屬於同一杯,全靠人自己記。下半把「一杯飲料」寫成類別 Drink,照它做出的每一杯都自己帶著品名、尺寸、加料;b.add("珍珠") 的意思是「對 b 這一杯加料」,不可能加錯杯。

Python 的解法是讓你自己定義一種新的東西,叫類別(class)。類別像一張設計圖,規定「一杯飲料」要帶著哪些資料、能做哪些動作;照著設計圖做出來的每一杯,叫物件(object),也常叫實例(instance)。物件身上帶著的資料叫屬性(attribute),寫在類別裡、讓物件能做的動作叫方法(method)。

其實你從第 4 課就在用物件了。"latte".upper()、第 5 課的 orders.append("雞腿飯"),都是「物件、點號、方法名稱、括號」的寫法:字串和串列是 Python 內建好的類別(str、list),每一個字串、每一個串列都是照它們做出來的物件。這一課要學的,是自己畫一張設計圖。

生活比喻:櫃台的點單本

晴空咖啡櫃台有一本點單本,每一張都照同一個格式印著三個欄位:「品名__ 尺寸__ 加料__」,最下面還印著一行作業說明:「加料:在本單的加料欄追加品項」。格式是印刷廠事先設計好的,整本只有這一種。客人來點餐,店員就撕下一張新的,填上這一杯的品名和尺寸;第二位客人來,再撕一張,兩張各寫各的,在第二張寫上珍珠,第一張不會跟著多出珍珠。

吧台的店員要替某一杯加料,會拿起那一張單,照底下的說明在「本單」的加料欄寫上品項。說明只寫「本單」,沒有寫是哪一位客人,因為它指的永遠是「手上正拿著的這一張」。另外,櫃台習慣叫這張單「3 號單」,吧台叫它「拿鐵那張」,兩個稱呼說的是同一張紙:吧台在上面加了燕麥奶,櫃台拿起 3 號單也會看到燕麥奶。

回到程式:點單的格式就是類別 Drink;撕下一張、填上內容,是呼叫類別建立物件 Drink("拿鐵", "M"),負責「填上內容」的是一個特別的方法 __init__(init 是 initialize 初始化的縮寫,前後各兩條底線);一張填好的單是一個物件,上面的欄位是屬性 self.name、self.size、self.toppings;單底的作業說明是方法 add,說明裡的「本單」就是 self,代表「這一次被操作的那一個物件」。兩個稱呼指同一張單,就是兩個名字(第 2 課的標籤)貼在同一個物件上。
點單本(比喻) Python 的對應 點單本的格式(只有一種) 類別 class Drink 撕下一張新單 呼叫 Drink("拿鐵", "M") 填上這一杯的品名、尺寸 __init__ 替新物件貼上屬性 一張填好的點單 物件 object(實例 instance) 單上的欄位 屬性 self.name、self.size 單底印的作業說明 方法 def add(self, item) 說明裡寫的「本單」 self:正被操作的那一個物件
左欄是點單本,右欄是 Python 的正式說法。前三列是「做出一個物件」的過程,後四列是做好之後的樣子。self 最難抓,記住它就是說明裡的「本單」:誰被拿起來操作,self 就是誰。

這個比喻有三個地方和 Python 不一樣,播放器會讓你親眼看到。第一,紙本的每一張單底下都印了一份作業說明,Python 的方法卻只存在類別裡一份,物件身上只有屬性;呼叫 a.add(…) 時,Python 是回到類別去找 add。第二,點單的欄位是事先印好的空格,Python 物件剛建立時身上什麼屬性都沒有,是 __init__ 執行時一個一個「貼」上去的,你會在播放器裡看到一個寫著「還沒有屬性」的新物件。第三,欄位裡是用筆「寫」上去的字,Python 的屬性和第 2 課的變數一樣是名字標籤:self.toppings = [] 是把物件身上的名字 toppings 貼到一個新串列上,所以加料時 append 會就地修改那個串列,這正是第 5 課的觀念。

名字(變數) 物件(真正的資料) 全域(主程式) Drinkab 類別 class · o1 Drink 方法:__init__、add Drink 物件 · o2 .name'拿鐵' .size'M' .toppings Drink 物件 · o7 .name'紅茶' .size'L' .toppings 串列 list · o5 [] 串列 list · o10 '珍珠'
實驗室一的程式跑完時,記憶體長這樣(和播放器同一套畫法)。設計圖 Drink 只有一個,方法列在它身上;照圖做出的兩個物件只帶著屬性,各有自己的 toppings 串列(o5 和 o10),所以替 b 加珍珠,a 那杯不受影響。

🎮 互動實驗室一:照設計圖做出兩杯

這段程式先畫一張設計圖 Drink,再照著它做出拿鐵和紅茶兩杯,最後只替紅茶加珍珠。它是 Python 3.13 真的執行一次、逐步記錄下來的結果。按「下一步」,綠色箭頭是下一個要執行的行,黃色是剛執行完的行;右下方左欄是名字和呼叫框(第 9 課),右欄是物件。黃色底的方塊是類別和照它做出的物件,物件裡 .name 這種一列一列的表格就是屬性。停在 🤔 的地方,先猜再按下一步。

畫面說明:第 3 步是建立物件的瞬間:右欄出現一個寫著「還沒有屬性」的 Drink 物件,__init__ 呼叫框裡的 self 指著它,這時全域還沒有 a。接著每執行一行 self.… = …,物件的表格就多一列。第 8 步呼叫框拿掉、a 貼上這個物件。第 9 步做第二杯時,同樣的第 3~5 行再執行一次,但 self 指向的是另一個新物件。第 15 步呼叫 b.add,看 self 的箭頭指向哪一杯。

🎮 互動實驗室二:同一杯,還是兩杯?

右上角的選單有三段程式。前兩段只差第 8 行:c = a 和 c = Drink("拿鐵")。兩段的 c 都「是一杯拿鐵」,記憶體裡卻完全不同:先用第一段走一遍,再切到第二段,比較右欄有幾個 Drink 物件、a 和 c 的箭頭指向哪裡。第三段是初學者最常犯的錯:在 __init__ 裡忘了寫 self.。

畫面說明:「① c = a」這段,第 8 步之後右欄仍然只有一個 Drink 物件,兩支箭頭指著它;呼叫 c.add 時,self 也指著同一個物件,所以 a 看得到燕麥奶。切到「② c = Drink("拿鐵")」,第 8 行會再走一次 __init__,右欄多出第二個物件,之後的改動都只發生在 c 那杯。切到「③ 忘了寫 self.」,盯著第 3 行執行前後的物件:它從頭到尾都沒有 .name 這一列。

🎮 互動實驗室三:物件工廠,自己寫程式

這一次換你寫程式。上面的設計圖 Drink 已經寫好了,用下面幾排按鈕組出一行程式、按「執行」,這一行就會加到程式最後面,下方的記憶體圖立刻更新(為了專心看結果,這裡每按一次只前進一整行,不展開 __init__ 和 add 內部的步驟,那些在實驗室一、二已經看過)。播放器的「上一步」可以回放你寫的整段程式。試著完成下面的四個任務;寫錯了可以按「撤銷上一行」。

① 照設計圖做一杯 = Drink(, )
② 呼叫方法加料 .add()
③ 直接改屬性 .size =
④ 把名字貼到另一個名字指著的物件 =
⑤ 印出來看看 print()
先用 ① 做一杯飲料,例如 a = Drink("拿鐵", "M")。
畫面說明:每按一次「執行」,播放器會自動停在最後一步,說明欄告訴你這一行讓哪個名字新增或改貼、哪個物件被修改。注意 ① 每執行一次都會在右欄多出一個新的 Drink 物件,④ 卻只會多一支箭頭;一杯飲料失去所有名字時,會以虛線顯示一次「沒有名字指向它了」。

📘 原理補完

1. 正式名稱一次對照

看官方文件或別人的教學時,會碰到下面這些名詞。左邊兩欄是正式說法,右邊兩欄對應到這一課的程式和播放器畫面。

用語英文在程式裡長怎樣白話意思
類別classclass Drink:設計圖:規定這種東西有哪些屬性、能做哪些動作
物件/實例object/instanceDrink("拿鐵", "M") 做出來的東西照設計圖做出的一個實體,可以做很多個
建立物件instantiationa = Drink("拿鐵", "M")呼叫類別,就像呼叫函式,結果是一個新物件
初始化方法__init__def __init__(self, name, size):建立物件時自動執行,負責替新物件貼上屬性
selfself每個方法的第一個參數這一次被操作的那一個物件
屬性attributeself.name、a.toppings貼在物件身上的名字,指向這個物件自己的資料
方法methoddef add(self, item):、a.add("珍珠")寫在類別裡的函式,透過物件呼叫
點號dot notationa.name、a.add(…)「a 身上的……」:取屬性或呼叫方法

類別名稱習慣用大寫字母開頭、每個英文字的字首大寫,例如 Drink、StampCard;變數、屬性和方法名稱則用小寫加底線,例如 toppings、add_topping。這不是規定,寫錯也能執行,但照這個習慣,別人一看就知道 Drink(…) 是在建立物件,不是呼叫普通函式。內建的 str、list 是早期留下來的例外。

2. 建立一個物件時發生的三件事

a = Drink("拿鐵", "M") 這一行看起來很短,Python 其實做了三件事:先做出一個空的 Drink 物件;接著自動呼叫 __init__,開一個呼叫框,把 self 貼到這個新物件、name 和 size 貼到傳進來的兩個字串,執行裡面的 self.name = name 等等,替物件貼上屬性;最後 __init__ 結束、呼叫框拿掉,「呼叫類別」這件事的結果就是那個新物件,等號左邊的 a 才貼上去。實驗室一的第 3 到第 8 步,就是這三件事。

① 做出空物件② 呼叫 __init__③ 交回物件 Python 自動做的,這時還沒有 aself 貼到新物件,self.… = … 貼上屬性呼叫框拿掉,a 才貼上去 全域(主程式) Drink Drink 物件 · o2 (還沒有屬性) Drink.__init__() 的呼叫框 selfnamesize Drink 物件 · o2 .name '拿鐵' .size 'M' .toppings [] '拿鐵' 'M' 全域(主程式) Drinka Drink 物件 · o2 .name '拿鐵' .size 'M' .toppings [] 還沒有任何名字貼著它
實驗室一第一杯拿鐵的三個瞬間。②的棕色箭頭是 self:它和最後的 a 指向同一個物件 o2,只是 self 先到,a 要等 __init__ 做完才貼上。③呼叫框拿掉後,參數 name、size 跟著消失,屬性卻留在物件身上,因為屬性是貼在物件上的名字,不是呼叫框裡的名字。

兩個細節值得記住。第一,__init__ 不寫 return,它的回傳值是 None;a 拿到的新物件不是 __init__ 交回來的,而是「呼叫類別」這整件事的結果。第二,__init__ 的參數就是建立物件時要給的東西:定義成 __init__(self, name, size),建立時就要寫 Drink("拿鐵", "M") 兩個引數,少給一個會出錯(下面第 7 小節);也可以像第 9 課那樣給預設值,例如 size="M"。

3. self:方法怎麼知道是哪一杯

方法 add 只在類別裡寫了一次,卻要能替任何一杯加料,所以它需要一個參數來接「這一次是哪一杯」,這個參數就是 self。寫 b.add("珍珠") 時,Python 先看點號前面的 b,到它的類別 Drink 裡找到 add,然後把 b 自動放在第一個位置傳進去,效果和 Drink.add(b, "珍珠") 完全一樣。這就是為什麼定義時要寫 self,呼叫時卻不用傳:那個位置 Python 已經幫你填好了。

你寫的Python 做的方法的定義 a.add("珍珠") Drink.add(a,"珍珠") defadd(self,item): 點號前面的物件放到第一個位置 self item 類別 class Drink 方法:__init__、add Drink 物件(a 貼著) .name '拿鐵'.size 'M'.toppings […] 找不到 add, 回類別找
左邊三行由上往下讀:a.add("珍珠") 被 Python 改寫成 Drink.add(a, "珍珠"),於是 a 落在 self 的位置、"珍珠" 落在 item。右邊說明 add 從哪裡來:物件身上只有屬性,方法存在類別裡,所有物件共用同一份。

self 只是習慣的名字,換成別的也能執行,但整個 Python 社群都用 self,請不要改。真正要注意的是兩個「忘了」:定義方法時忘了寫 self 參數,呼叫時 Python 照樣會塞一個物件進去,參數數量就對不上;方法裡忘了寫 self.,例如 name = name,那只是呼叫框裡的區域變數(第 9 課),方法結束就消失,物件身上什麼也沒留下,實驗室二第三段就是這種情況。

4. 方法讀取、修改自己的屬性

方法需要的資料如果都在物件身上,就不必另外傳參數,從 self 讀就好。下面的 price 只有一個參數 self,卻能依「這一杯」的尺寸和加料數量算出價錢(價格都是示意)。而 add 裡的 self.toppings.append(item) 是對物件身上那個串列做就地修改:和第 5 課一樣,串列還是同一個物件,只是內容變了,所以方法結束後,從外面看 order.toppings 也是新的內容。

畫面說明:第 9 步和第 13 步兩次呼叫 add,self 都貼在 order 指向的同一個物件上,串列 o5 被就地修改,編號一直沒變。第 17 步呼叫 order.price():括號裡什麼都沒傳,呼叫框裡卻有 self。第 21 步 base 從 60 改貼到 75,它是 price 的區域變數,方法結束就消失;self.size、self.toppings 這些屬性則一直留在物件上。

屬性的「改」也分兩種,和第 5 課完全一樣:self.toppings.append(…) 是改物件(就地修改那個串列);self.size = "L" 是把物件身上的名字 size 撕下來改貼到新的字串,字串本身沒有被修改。不論哪一種,改的都是這個物件自己的屬性,別的物件不受影響。

5. 同一杯,還是兩杯:is 與 ==

每呼叫一次類別,就做出一個新物件;等號只會讓名字貼上去,不會複製。所以 c = a 之後只有一杯、兩個名字,透過哪個名字改,另一個都看得到;c = Drink("拿鐵") 則是第二杯,就算內容和 a 一模一樣,也是兩個獨立的物件。要確認兩個名字是不是貼在同一個物件上,用第 5 課學過的 is。

c = a c = Drink("拿鐵") 全域 ac Drink 物件 · o2 .name '燕麥拿鐵' .toppings […] 全域 ac Drink 物件 · o2 .name '拿鐵' .toppings [] Drink 物件 · o6 .name '燕麥拿鐵' .toppings […] a is c → True 一個物件、兩個名字: 從 c 改名字、加料,a 都看得到 a is c → False 兩個物件:改 c 那杯,a 那杯不變
實驗室二前兩段跑完時的記憶體。左邊右欄只有一個 Drink 物件,兩支箭頭指著它;右邊有兩個物件,各有自己的屬性。兩段程式之間只差第 8 行。

那 == 呢?自己寫的類別如果沒有另外規定「怎樣算相等」,== 的結果和 is 一樣,只有同一個物件才算相等:Drink("拿鐵", "M") == Drink("拿鐵", "M") 是 False。想比內容,就比屬性,例如 p.name == q.name。讓 == 改成比內容的寫法(定義 __eq__,或用標準函式庫的 dataclass)在地圖上的「類別與物件」進階節點。

6. 為什麼不要直接 print(物件)

把自己做的物件直接交給 print,Python 不知道你想看哪些屬性,只好印出「這是哪個類別的物件、放在記憶體的哪裡」:

class Drink:
    def __init__(self, name):
        self.name = name
a = Drink("拿鐵")
print(a)
print(a.name)
Python 3.13 的輸出畫面
<__main__.Drink object at 0x7fbcdb8a97f0>
拿鐵

第一行的 0x7fbcdb8a97f0 是記憶體位置,每次執行、每台電腦都不一樣,對讀程式的人沒什麼幫助;__main__ 代表這個類別定義在你直接執行的那支檔案裡。想看內容,就印屬性。如果希望 print(a) 直接印出好讀的文字,可以在類別裡定義一個特別的方法 __str__,回傳一個字串,print 就會改用它:

class Drink:
    def __init__(self, name):
        self.name = name
    def __str__(self):              # print(物件) 時會呼叫它,必須回傳字串
        return "一杯" + self.name
a = Drink("拿鐵")
print(a)                            # 一杯拿鐵

像 __init__、__str__ 這種前後各兩條底線的方法,叫特殊方法(special method,口語常說 dunder method,double underscore 的縮寫):你不會直接呼叫它們,而是 Python 在特定時機自動呼叫,例如建立物件時呼叫 __init__、印出時呼叫 __str__。

7. 常見錯誤與錯誤訊息的長相

下面每一段都是用 Python 3.13 實際執行的畫面(File 後面的路徑是檔案在電腦上的位置,你的電腦會顯示自己的資料夾)。和第 10 課一樣,先看最後一行:冒號前是錯誤類型,冒號後是說明。

定義方法時忘了寫 self。呼叫 a.add("珍珠") 時,Python 會自動把 a 放進第一個位置,加上 "珍珠" 一共傳了 2 個,可是 add 只準備了 1 個參數:

class Drink:
    def __init__(self, name, size):
        self.name = name
        self.size = size
    def add(item):
        print("加料", item)
a = Drink("拿鐵", "M")
a.add("珍珠")
Traceback (most recent call last):
  File "/tmp/b0_class/main.py", line 8, in <module>
    a.add("珍珠")
    ~~~~~^^^^^^^^
TypeError: Drink.add() takes 1 positional argument but 2 were given

在 __init__ 裡忘了寫 self.。錯誤不會發生在 __init__ 那一行,而是到後面真的去讀屬性時才出現,所以要回頭找「當初有沒有貼上這個屬性」:

class Drink:
    def __init__(self, name, size):
        name = name
        size = size
a = Drink("拿鐵", "M")
print(a.name)
Traceback (most recent call last):
  File "/tmp/b0_class/main.py", line 6, in <module>
    print(a.name)
          ^^^^^^
AttributeError: 'Drink' object has no attribute 'name'

屬性名稱拼錯。Python 3.13 會在拼法很接近時提示「Did you mean」(意思是「你是不是要寫 name」);差太多就不會提示:

class Drink:
    def __init__(self, name):
        self.name = name
a = Drink("拿鐵")
print(a.nme)
Traceback (most recent call last):
  File "/tmp/b0_class/main.py", line 5, in <module>
    print(a.nme)
          ^^^^^
AttributeError: 'Drink' object has no attribute 'nme'. Did you mean: 'name'?

建立物件時少給引數。訊息裡寫的是 __init__,因為那些參數是它要的;self 不算在內,Python 會自動給:

class Drink:
    def __init__(self, name, size):
        self.name = name
        self.size = size
a = Drink("拿鐵")
Traceback (most recent call last):
  File "/tmp/b0_class/main.py", line 5, in <module>
    a = Drink("拿鐵")
        ~~~~~^^^^^^^^
TypeError: Drink.__init__() missing 1 required positional argument: 'size'

忘了加括號。a = Drink 沒有建立物件,只是讓 a 也貼到類別本身。透過類別呼叫方法時沒有「點號前面的物件」可以自動塞進 self,於是 "珍珠" 被拿去當 self,item 反而沒有東西:

class Drink:
    def __init__(self, name):
        self.name = name
        self.toppings = []
    def add(self, item):
        self.toppings.append(item)
a = Drink
a.add("珍珠")
Traceback (most recent call last):
  File "/tmp/b0_class/main.py", line 8, in <module>
    a.add("珍珠")
    ~~~~~^^^^^^^^
TypeError: Drink.add() missing 1 required positional argument: 'item'

__init__ 拼錯。把 __init__ 打成 __int__(少了一個 i)或底線只寫一條,Python 就不認得它是初始化方法,類別等於沒有 __init__,建立物件時不能傳任何引數。看到「takes no arguments」,先檢查 __init__ 的拼法:

class Drink:
    def __int__(self, name):
        self.name = name
a = Drink("拿鐵")
Traceback (most recent call last):
  File "/tmp/b0_class/main.py", line 4, in <module>
    a = Drink("拿鐵")
        ~~~~~^^^^^^^^
TypeError: Drink() takes no arguments

8. 兩段完整範例

第一段把這一課的 Drink 補完整:加上預設參數、算價錢的方法和 __str__,再把好幾杯放進一個串列,用第 8 課的 for 迴圈逐杯處理。串列的每一格都指向一個物件,就像第 11 課的巢狀資料:

# 晴空咖啡:用類別表示「一杯飲料」
class Drink:
    def __init__(self, name, size="M"):   # size 沒傳時用 "M"(第 9 課的預設參數)
        self.name = name                  # 屬性:貼在這杯飲料身上的名字
        self.size = size
        self.toppings = []                # 每杯各自做一個新的空串列
    def add(self, item):
        self.toppings.append(item)        # 就地修改這杯自己的加料清單
    def price(self):
        total = 60                        # 中杯基本價(示意)
        if self.size == "L":
            total = total + 15            # 大杯加 15 元
        return total + 10 * len(self.toppings)   # 每項加料 10 元
    def __str__(self):                    # print(物件) 時要顯示的文字
        return f"{self.name}({self.size})加料 {len(self.toppings)} 項"

orders = [Drink("拿鐵"), Drink("紅茶", "L")]   # 串列裡放兩個 Drink 物件
orders[1].add("珍珠")                      # orders[1] 是第二杯,只替它加料
orders[1].add("椰果")
total = 0
for d in orders:                           # 第 8 課的 for:d 每圈改貼到下一杯
    print(d, d.price(), "元")               # print(d) 會用 __str__ 回傳的文字
    total = total + d.price()
print("合計", total, "元")
拿鐵(M)加料 0 項 60 元
紅茶(L)加料 2 項 95 元
合計 155 元

第二段是集點卡,重點放在「同一張卡還是兩張卡」。注意 stamps 是整數,整數不能修改,所以 self.stamps = self.stamps + n 是把屬性改貼到新的整數;不管是改貼還是就地修改,改的都是那一個物件身上的屬性:

# 晴空咖啡的集點卡:同一張卡,還是兩張卡?
class StampCard:
    def __init__(self, owner):
        self.owner = owner
        self.stamps = 0                   # 每張新卡都從 0 點開始
    def stamp(self, n=1):
        self.stamps = self.stamps + n     # 整數不能修改:屬性改貼到新的整數
    def redeem(self):
        if self.stamps >= 10:             # 滿 10 點可以換一杯
            self.stamps = self.stamps - 10
            return True
        return False

mei = StampCard("小美")
zhe = StampCard("阿哲")                    # 兩張不同的卡
family = mei                              # 沒有做新卡:family 和 mei 貼在同一張卡上
mei.stamp(6)
family.stamp(5)                           # 透過 family 蓋章,蓋在小美那張卡上
zhe.stamp(3)
print(mei.owner, mei.stamps, zhe.owner, zhe.stamps)
print(family.redeem(), mei.stamps)        # 換了一杯,剩 1 點
print(zhe.redeem(), zhe.stamps)           # 點數不夠,不能換
print(family is mei, zhe is mei)
小美 11 阿哲 3
True 1
False 3
True False

類別還有一個常聽到的功能叫繼承(inheritance):寫 class Tea(Drink):,Tea 就自動擁有 Drink 的所有方法,再加上自己的。入門階段先會用本課的內容就夠了,繼承留到地圖上的「類別與物件」進階節點。

9. 以後在 AI 程式裡會看到它

學完這一課,你已經能看懂 AI 與資料分析範例程式最常見的句型。scikit-learn(機器學習套件)的模型全部都是類別:model = LinearRegression() 就是呼叫類別、建立一個線性迴歸模型物件;model.fit(X, y) 是呼叫它的方法,用資料訓練,學到的結果會存成物件身上的屬性,例如 model.coef_;model.predict(…) 再用這些屬性做預測。fit 改的是 model 這個物件自己,所以不用寫成 model = model.fit(…),這和 a.add("珍珠") 改的是 a 那杯是同一件事。

# 套件要先匯入(import),地圖上的「模組、套件與 import」節點會講
X = [[1], [2], [3]]               # 3 筆資料,每筆一個特徵(示意)
y = [3, 5, 7]
model = LinearRegression()        # 呼叫類別、建立物件:這時還沒學到任何東西
print(hasattr(model, "coef_"))    # False:還沒有 coef_ 這個屬性
model.fit(X, y)                   # 呼叫方法:用資料訓練,結果貼成物件的屬性
print(model.coef_)                # [2.]:學到的斜率
print(model.predict([[4]]))       # [9.]:用學到的屬性預測
fit 之前 fit 之後(同一個物件) model LinearRegression 物件 .fit_intercept True .copy_X True …其他設定值 (還沒有 coef_) model LinearRegression 物件 .fit_intercept True .copy_X True …其他設定值 .coef_ [2.] .intercept_ ≈ 1.0 …其他學到的值 model.fit(X, y) 就地修改:替同一個物件貼上新屬性 之後 model.predict([[4]]) 讀這些屬性算出 [9.](數字為示意資料的實跑結果)
和實驗室一的畫法相同:model 這個名字從頭到尾貼著同一個物件,fit 只是讓物件身上多了學到的屬性(黃底那幾列)。scikit-learn 的慣例是「學到的屬性名稱結尾有一條底線」,看到 coef_ 就知道它要 fit 之後才會有。

PyTorch(深度學習套件)也一樣:自己定義的神經網路是一個類別,__init__ 裡用 self.layer = … 把每一層存成屬性。更深入的用法在地圖上的 fit/predict 介面、線性與邏輯迴歸、nn.Module 建模 節點。

10. 學完 14 課之後,可以往哪裡走

到這裡,零基礎系列就全部結束了。你已經能看著一段 Python 程式,在腦中畫出名字、物件和箭頭,判斷每一行會讓什麼東西改變。接下來不必照固定順序,依你的目標挑一條路:

零基礎起步 14 課完成 語法核心 b2 推導式、lambda、字串處理:讀別人的程式更快 進階觀念 b3 類別進階、import、例外處理、檔案讀寫 資料處理與視覺化 NumPy 陣列、pandas 表格:AI 程式的日常
三條路可以並行:想讀懂更多寫法走 b2,想寫出能長期維護的程式走 b3,想直接處理資料、往 AI 前進就到資料處理分頁。每一條路的節點都有範例程式,看不懂某一行時,回到這 14 課找對應的觀念。

語法核心 b2

零基礎課刻意沒教的簡寫,在別人的程式裡天天出現。

進階觀念 b3

從這一課的類別出發,學會把程式拆檔、處理錯誤、讀寫檔案。

資料處理分頁

AI 與資料分析的第一站:陣列和表格,都是照類別做出的物件。

開始寫自己的程式之前,也建議看一下地圖上的 Python 安裝、編輯器與 IDE 和 Jupyter Notebook 與 Colab 節點,挑一個順手的地方練習。

✅ 自我檢測

6 題原創的程式閱讀題,每題讀一小段程式、選出結果。所有答案都用 Python 3.13 實際執行確認過;輸出有好幾行時,選項裡用 ⏎ 表示換行。選完會立即顯示對錯與解析,全部作答後出現總分。目前得分:0 / 6