💡 先搞懂問題
第 1 課的程式有個缺點:print("兩杯共", 65 * 2, "元") 算出的 130 印到畫面上之後就不見了,下一行想再用它,只能重算一次;單價一改,程式裡每個 65 都得找出來改。我們需要替資料取一個名字,之後用名字就能找到它,這個名字就叫變數(variable)。寫法是 price = 65。
問題是這一行看起來太像數學。很多人第一次看到會有三個疑問:price = 65 是「price 等於 65」嗎?那 count = count + 1 不就是 0 = 1,怎麼可能成立?還有,寫了 b = a 之後再改 a,b 會不會跟著變?不少教材說「變數是裝資料的箱子」,用箱子去想,這三題反而更難回答。這一課換一個更接近 Python 實際行為的說法:變數是貼在資料上的名字標籤。
先對好名詞。程式裡真正的資料,例如整數 65、字串 "桂花拿鐵",在 Python 裡都是一個物件(object),每個物件都有自己的型別(type,整數、字串……第 3 課細講)。名字(name)本身不存資料,只負責指向某個物件。= 這個符號叫指派(assignment):先把右邊算出一個物件,再把左邊的名字貼到那個物件上。
生活比喻:倉庫貨架上的便利貼
虛構的「晴空咖啡」倉庫裡,貨架上擺著一罐罐糖漿和一張張寫著數字的號碼牌。店長不在罐子上直接寫字,而是貼便利貼:一張寫著「今日特調」的便利貼貼在桂花糖漿罐上。店員想知道今日特調是什麼,就去找那張便利貼,看它貼在哪一罐。隔天特調換成蜂蜜,店長把「今日特調」撕下來,改貼到蜂蜜罐上;桂花糖漿罐還在架上,罐子裡的東西一點都沒變,只是不再叫「今日特調」。一罐東西也可以同時貼兩張便利貼,例如「今日特調」和「店長推薦」貼在同一罐上。
記帳也用同一套方法。架上有寫著 0、1、2……的號碼牌,「杯數」便利貼一開始貼在 0 號牌上。賣出一杯時,店員先看「杯數」貼在幾號(0),算出 0 加 1 是 1,再把「杯數」撕下來改貼到 1 號牌。他不會拿筆把 0 改成 1,號碼牌本身從來不被塗改。如果有人問「『本月目標』貼在哪?」而倉庫裡根本沒有這張便利貼,店員只能回答「找不到」。
price = 65 的意思是「先拿到 65 這個物件,再把 price 這張便利貼貼上去」,這個動作叫指派。把「今日特調」改貼到蜂蜜罐,就是重新指派(price = 70):price 改指向新物件,舊的 65 本身沒有被改。賣出一杯時「先看貼在幾號、算出下一號、再改貼」,就是 cups = cups + 1:先算右邊,再貼左邊。兩張便利貼貼同一罐,就是 b = a 之後的狀態。找不到便利貼,就是 NameError。
比喻有三個地方要修正。第一,倉庫裡沒人要的罐子會一直擱在架上,Python 則會自動清掉沒有任何名字指向的物件(垃圾回收,garbage collection),播放器把它畫成一閃而過的淡色虛線框。第二,真實的罐子可以打開來換內容,但整數和字串在 Python 裡是不可變(immutable)的物件:它們的內容永遠不會被修改,看起來像「改值」的寫法,其實都是產生新物件、再把名字改貼過去。之後第 5 課的串列(list)是可變的,內容可以就地修改,到時候「兩張便利貼貼同一罐」就會變得很重要。第三,Python 為了省記憶體,有時會讓兩個名字共用同一個小整數或短字串物件,例如兩個名字剛好都等於 5。因為這些物件不能被修改,共用不會出任何問題,看到兩條箭頭指向同一個 5 不必驚訝。
price = 65 的兩個動作;③ 是下一行 price = 70。紫色箭頭是這一步剛改變的箭頭,和播放器的畫法一致。🎮 互動實驗室一:看名字標籤貼上、撕下、改貼
晴空咖啡的收銀程式只有 9 行。按「下一步 ▶」一次執行一行,注意右側的記憶體區:左欄「名字」、右欄「物件」,箭頭表示名字目前貼在哪個物件上。剛改變的箭頭會以紫色畫出,剛出現的名字或物件會彈出來,失去名字的物件會變成淡色虛線框。出現黃色的「🤔 先猜猜看」時,先選答案再按下一步。
price = 70 剛執行完)時,看 price 的箭頭從 65 移到新的 70,65 變成虛線框並標上「沒有名字指向它了」:物件 65 本身沒有被改。第 7、8 步的 cups = cups + 1 也一樣,每一步都是新的整數物件,cups 這張標籤一路改貼。最後的 total 是 140 而不是 130,因為第 8 行執行時 price 已經改貼到 70。🎮 互動實驗室二:複製、交換與 NameError,五種寫法對照
用播放器右上角的選單切換五段程式,每一段都從頭按到尾,盯著箭頭看:① b = a 之後再改 a,b 會不會跟著變;② 直接互相指派想交換兩杯飲料,結果怎麼失敗;③ 用暫存變數 temp 正確交換;④ Python 的寫法 left, right = right, left;⑤ 在名字建立之前就用到它。
b = a 執行完時記憶體只有一個 10、兩條箭頭,這就是「兩張便利貼貼同一罐」;接著 a 改貼到 15,b 的箭頭不動。② 的關鍵在第 4 步:left 改貼到 "美式" 的瞬間,"拿鐵" 變成虛線框,之後再也找不回來。③ 多了一張 temp 標籤先把 "拿鐵" 留住。⑤ 的錯誤訊息最後一行寫著 NameError: name 'unit_price' is not defined:第 2 行要用 unit_price 時,左欄根本還沒有這個名字。🎮 互動實驗室三:命名判定卡
每張卡片是一個想拿來當變數的名字。判斷它屬於哪一類:合法而且好讀、合法但不建議、或根本不合法(Python 會回報 SyntaxError)。答完會顯示用這個名字實際寫成的程式,以及 Python 3.13 執行的真正結果。共 12 張,順序隨機。
📘 原理補完
1. 指派敘述:官方怎麼說
price = 65 這種一行,正式名稱是指派敘述(assignment statement)。官方文件的說法是:先計算右邊的運算式得到一個物件,再把左邊的名稱綁定(bind)到這個物件。「綁定」就是本課說的「貼標籤」。左邊的名字正式叫識別字(identifier),日常大家就叫它變數。Python 不需要事先宣告變數,也不用說明它要放哪種資料:第一次指派就是建立,之後再指派就是改貼。
所以 = 和數學的等號是兩回事。數學的 x = x + 1 是一個無解的方程式;Python 的 x = x + 1 是一個有先後順序的動作。真正用來「比較兩邊是否相等」的符號是兩個等號 ==,第 7 課條件判斷會用到。
| 寫法 | 數學課本的意思 | Python 的意思 | 結果(Python 3.13) |
|---|---|---|---|
x = 5 | x 和 5 相等 | 把名字 x 貼到物件 5 上 | 沒有輸出,x 指向 5 |
x = x + 1 | 無解(x 不可能等於 x + 1) | 先算右邊(現在的 x 加 1),再把 x 改貼到結果上 | x 指向 6 |
x += 1 | 沒有這種寫法 | x = x + 1 的簡寫 | x 指向 7 |
5 = x | 和 x = 5 一樣 | 左邊必須是名字,數字不能被貼標籤 | SyntaxError: cannot assign to literal here |
x == 7 | (相當於數學的 =) | 比較兩邊是否相等,得到 True 或 False | True(第 7 課) |
y = x | y 和 x 相等 | 把 y 也貼到 x 目前指向的物件上 | x、y 指向同一個 7 |
判斷一行指派做了什麼,固定用三個步驟:
- 只看等號右邊:把裡面的名字都換成它們此刻指向的物件,算出一個結果。
- 看等號左邊的名字:如果還不存在就建立,已經存在就把它從舊物件撕下來。
- 把名字貼到第 1 步的結果上。其他名字的箭頭一律不動。
2. 計數:count = count + 1
cups = cups + 1 拆成兩個動作。① 右邊用的是改貼之前的 cups(指向 0),算出新的整數物件 1;② 才把 cups 改貼過去。紫色是剛改變的箭頭,淡色虛線框是失去名字的物件。這個「先算右邊、再改貼」的寫法叫累加或計數,第 8 課的迴圈會大量使用。cups += 1 是它的簡寫,效果完全相同,-=、*= 也是同樣的道理。還要注意一點:變數不是試算表的公式。total = unit_price * cups 只在執行那一刻計算一次,之後就算 unit_price 改貼到別的值,total 也不會自動重算,下方範例 1 會實際示範。
3. a = b 之後再改 a,b 為什麼不變
b = a 只是讓 b 也貼到 a 正指著的 10,記憶體裡沒有第二個 10。右:a = a + 5 產生新的 15,只有 a 改貼,b 的箭頭沒有被碰到。原因有兩個:指派只會移動左邊那一個名字的箭頭;而整數、字串都是不可變(immutable)的,沒有任何寫法能把 10 這個物件本身改成 15。所以對整數和字串來說,b = a 之後怎麼改 a,b 都不受影響。
這個結論只適用於不可變的物件。第 5 課的串列(list)可以就地修改內容:如果 a 和 b 貼在同一個串列上,用 a 去「加一個元素」,b 也會看到,因為它們本來就是同一個物件。到時候這張圖會再出現一次,請先記住「兩張標籤、一個物件」這個畫面。
另外,Python 會重複使用某些不可變的物件,例如分別寫 x = 5 和 y = 5,兩個名字通常會指向同一個 5,播放器也會照實畫出兩條箭頭指向同一個方塊。這是 Python 為了省記憶體的做法;因為 5 不能被修改,共用不會造成任何問題,寫程式時不需要在意。
4. 交換兩個變數
直接寫 left = right 再 right = left 會失敗,因為第一行執行完,原本 left 指的物件已經沒有名字找得到了。傳統做法是多用一個暫存變數(temporary variable)先留住它。Python 另外提供更簡潔的寫法 left, right = right, left:右邊兩個值先全部找好、暫時打包成一組(元組 tuple,第 5 課),然後才依序貼到左邊的兩個名字上,所以不會互相覆蓋。實務上幾乎都用這一種。
5. 命名規則與好名字
名字有硬性規則,違反就是 SyntaxError,程式一行都不會執行:
- 只能用英文字母、數字和底線
_(Python 3 也允許中文等文字,但慣例上用英文)。空格、減號、句點、全形符號都不行。 - 不能以數字開頭:
cup2可以,2cup不行。 - 大小寫不同就是不同的名字:
price、Price、PRICE是三個名字。 - 不能使用保留字(keyword)。Python 3.13 共有 35 個:
False None True and as assert async await break class continue def del elif else except finally for from global if import in is lambda nonlocal not or pass raise return try while with yield。不用背,編輯器會把它們上成特別的顏色,誤用時也會立刻出現 SyntaxError。
合法之後,還要是「好名字」。Python 官方風格指南 PEP 8 建議:變數用全小寫、單字之間用底線連接(snake_case),例如 unit_price、total_cups;設定好不打算再改的值用全大寫(MAX_CUPS),這只是給人看的約定,Python 不會阻止你改。名字要說明「這是什麼」,x、data2、tmp 只適合很短的示範。最後,不要用 print、list、str、sum、max 這些內建函式的名字當變數:它們不是保留字,寫了不會馬上出錯,但會把原本的功能蓋掉。
6. 常見錯誤與錯誤訊息
名字打錯:少一個字母或拼錯,就是另一個從沒建立過的名字。Python 3.13 會找一個拼法很像的名字提示你:
count = 1
count = count + 1
print(cuont)
Traceback (most recent call last):
File "main.py", line 3, in <module>
print(cuont)
^^^^^
NameError: name 'cuont' is not defined. Did you mean: 'count'?先用後建:程式由上往下執行,用到某個名字的那一刻,它必須已經被指派過。即使下面幾行有寫,也來不及:
cups = 2
total = unit_price * cups
unit_price = 65
print(total)
Traceback (most recent call last):
File "main.py", line 2, in <module>
total = unit_price * cups
^^^^^^^^^^
NameError: name 'unit_price' is not defined以下幾個是 SyntaxError,Python 在執行前就會發現,整支程式一行都不會跑。名字用數字開頭:
2nd_cup = 1
File "main.py", line 1
2nd_cup = 1
^
SyntaxError: invalid decimal literal用了保留字:
class = 1
File "main.py", line 1
class = 1
^
SyntaxError: invalid syntax把等號兩邊寫反,或想用數學的方式「解方程式」。注意訊息還貼心地問你是不是想寫比較用的 ==:
5 = x
File "main.py", line 1
5 = x
^
SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='?名字裡有減號,被當成減法算式:
my-name = 1
File "main.py", line 1
my-name = 1
^^^^^^^
SyntaxError: cannot assign to expression here. Maybe you meant '==' instead of '='?最後一個不是 SyntaxError,而是執行到第 2 行才出錯:第 1 行把 print 這張標籤從內建函式撕下來改貼到 65,第 2 行想呼叫 print 時,它指著的已經是整數了。
print = 65
print("hi")
Traceback (most recent call last):
File "main.py", line 2, in <module>
print("hi")
~~~~~^^^^^^
TypeError: 'int' object is not callable7. 兩段完整範例
範例 1:點餐計數。三種累加寫法,加上「變數不是試算表公式」的示範:最後一行單價已經改貼到 70,total 仍然是當初算好的 195。
# 範例 1:晴空咖啡的點餐計數
unit_price = 65 # 單價:名字 unit_price 貼到 65
cups = 0 # 杯數從 0 開始
cups = cups + 1 # 先算右邊 0 + 1,再把 cups 改貼到 1
cups = cups + 1 # 1 + 1,cups 改貼到 2
cups += 1 # cups = cups + 1 的簡寫,cups 改貼到 3
total = unit_price * cups # 用的是「此刻」的值:65 * 3
print("杯數", cups, "總價", total)
unit_price = 70 # 改貼 unit_price
print("單價", unit_price, "總價", total) # total 不會自動重算
杯數 3 總價 195 單價 70 總價 195
範例 2:交換與共用。先用 Python 的寫法交換兩杯飲料,再示範 backup 和 morning 貼在同一個字串上之後,morning 改貼不會影響 backup。
# 範例 2:交換兩個變數,以及「a = b 之後再改 a」
morning = "美式"
evening = "拿鐵"
morning, evening = evening, morning # 右邊先全部找好,再依序貼上
print("早上", morning, "晚上", evening)
backup = morning # backup 和 morning 貼在同一個字串上
morning = "抹茶" # morning 改貼到新字串,backup 不受影響
print(morning, backup)
早上 拿鐵 晚上 美式 抹茶 拿鐵
以後讀 AI 或資料分析的範例程式時,幾乎每一行都是指派:df = pd.read_csv("sales.csv") 是把讀進來的整張表格貼上 df 這個名字,model = LogisticRegression() 是把建立好的模型貼上 model。讀這類程式時,同樣先看右邊做了什麼,再看結果被貼到哪個名字上;名字取得好不好,也直接決定別人看不看得懂你的分析流程。
✅ 自我檢測
以下 6 題都是原創的小程式,答案都用 Python 3.13 實際執行核對過。作答前可以先在紙上畫出名字和箭頭。選完會立即顯示對錯、解析和真正的輸出畫面,全部作答後會出現總分。目前得分:0 / 6