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

變數:貼在資料上的名字標籤

price = 65 是把 65 放進 price,還是別的意思?count = count + 1 在數學上不成立,為什麼程式裡天天出現?這一課用記憶體動畫一步一步看:名字是標籤,指派是先算右邊、再把標籤貼上去。

記憶體動畫播放器 五種寫法對照:複製、交換與 NameError 命名判定卡 12 張

💡 先搞懂問題

第 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):先把右邊算出一個物件,再把左邊的名字貼到那個物件上。

箱子模型(常見,但不準確) 標籤模型(Python 實際的樣子) a = 10 b = a 10 a 10 b 兩個箱子,各裝一份? a = 10 b = a 全域(主程式) ab 整數 int · o1 10 只有一個 10,兩張標籤貼在它上面 箱子模型在 b = a、交換變數、以及第 5 課的串列都會給出錯的直覺;播放器畫的是右邊這一種。
同樣兩行程式,兩種想法。左邊以為 b = a 會把 10「複製」一份放進 b 的箱子;右邊才是 Python 實際做的事:b 只是另一張標籤,貼到 a 正在指的那個 10 上。右邊的畫法和下方播放器完全相同:左欄名字、右欄物件、中間箭頭。

生活比喻:倉庫貨架上的便利貼

虛構的「晴空咖啡」倉庫裡,貨架上擺著一罐罐糖漿和一張張寫著數字的號碼牌。店長不在罐子上直接寫字,而是貼便利貼:一張寫著「今日特調」的便利貼貼在桂花糖漿罐上。店員想知道今日特調是什麼,就去找那張便利貼,看它貼在哪一罐。隔天特調換成蜂蜜,店長把「今日特調」撕下來,改貼到蜂蜜罐上;桂花糖漿罐還在架上,罐子裡的東西一點都沒變,只是不再叫「今日特調」。一罐東西也可以同時貼兩張便利貼,例如「今日特調」和「店長推薦」貼在同一罐上。

記帳也用同一套方法。架上有寫著 0、1、2……的號碼牌,「杯數」便利貼一開始貼在 0 號牌上。賣出一杯時,店員先看「杯數」貼在幾號(0),算出 0 加 1 是 1,再把「杯數」撕下來改貼到 1 號牌。他不會拿筆把 0 改成 1,號碼牌本身從來不被塗改。如果有人問「『本月目標』貼在哪?」而倉庫裡根本沒有這張便利貼,店員只能回答「找不到」。

倉庫(比喻) Python 便利貼「今日特調」 名字(變數) 罐子、號碼牌 物件(真正的資料) 把便利貼貼上去 指派 price = 65 撕下來改貼別罐 重新指派 price = 70 兩張便利貼貼同一罐 b = a:同一個物件 找不到這張便利貼 NameError
左欄是倉庫裡的動作,右欄是對應的 Python 寫法。注意「改貼」:被撕掉便利貼的罐子還在,只是沒有名字找得到它。
回到程式:便利貼就是名字(變數),罐子和號碼牌就是物件。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 = 70:撕下改貼 price = 65 price = 65 price = 70 (還沒有名字) 整數 int65 price 整數 int65 price 整數 int70 整數 int65 ✕ 沒有名字指向它了 物件先出現 箭頭=「貼在哪裡」 指派一律「右邊先、左邊後」。重新指派只改箭頭,舊物件 65 不會變成 70。
① 和 ② 是同一行 price = 65 的兩個動作;③ 是下一行 price = 70。紫色箭頭是這一步剛改變的箭頭,和播放器的畫法一致。

🎮 互動實驗室一:看名字標籤貼上、撕下、改貼

晴空咖啡的收銀程式只有 9 行。按「下一步 ▶」一次執行一行,注意右側的記憶體區:左欄「名字」、右欄「物件」,箭頭表示名字目前貼在哪個物件上。剛改變的箭頭會以紫色畫出,剛出現的名字或物件會彈出來,失去名字的物件會變成淡色虛線框。出現黃色的「🤔 先猜猜看」時,先選答案再按下一步。

畫面說明:按到第 5 步(第 4 行 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 張,順序隨機。

答對 0 / 0
連續答對 0
畫面說明:判斷順序:先看有沒有不允許的字元(空格、減號、全形符號)、是不是數字開頭、是不是保留字;全部通過才算合法。合法之後再問:別人看得懂嗎?會不會和內建函式或其他名字撞在一起?

📘 原理補完

1. 指派敘述:官方怎麼說

price = 65 這種一行,正式名稱是指派敘述(assignment statement)。官方文件的說法是:先計算右邊的運算式得到一個物件,再把左邊的名稱綁定(bind)到這個物件。「綁定」就是本課說的「貼標籤」。左邊的名字正式叫識別字(identifier),日常大家就叫它變數。Python 不需要事先宣告變數,也不用說明它要放哪種資料:第一次指派就是建立,之後再指派就是改貼。

所以 = 和數學的等號是兩回事。數學的 x = x + 1 是一個無解的方程式;Python 的 x = x + 1 是一個有先後順序的動作。真正用來「比較兩邊是否相等」的符號是兩個等號 ==,第 7 課條件判斷會用到。

寫法數學課本的意思Python 的意思結果(Python 3.13)
x = 5x 和 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 或 FalseTrue(第 7 課)
y = xy 和 x 相等把 y 也貼到 x 目前指向的物件上x、y 指向同一個 7

判斷一行指派做了什麼,固定用三個步驟:

  1. 只看等號右邊:把裡面的名字都換成它們此刻指向的物件,算出一個結果。
  2. 看等號左邊的名字:如果還不存在就建立,已經存在就把它從舊物件撕下來。
  3. 把名字貼到第 1 步的結果上。其他名字的箭頭一律不動。

2. 計數:count = count + 1

執行前 cups = 0 之後 全域(主程式) 整數 int 0 cups ① 先算右邊 cups + 1 → 1 全域(主程式) 整數 int 0 整數 int 1 cups ② 再貼左邊 cups = … 全域(主程式) 整數 int 0 沒有名字指向 整數 int 1 cups 右邊用的是「改貼之前」的 cups;整行算完,cups 才指向新的 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 之後 a = 10;b = a 全域(主程式) 整數 int 10 a b a = a + 5 之後 a 改貼,b 不動 全域(主程式) 整數 int 10 整數 int 15 a b 整數不能被修改:「改 a」只能是 a 改貼到新物件,所以 b 看到的永遠是原來的 10。
左: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. 交換兩個變數

第 3 行 temp = left temp 先留住拿鐵 全域(主程式) 字串 str '拿鐵' 字串 str '美式' left right temp 第 4 行 left = right 拿鐵還有 temp 全域(主程式) 字串 str '拿鐵' 字串 str '美式' left right temp 第 5 行 right = temp 交換完成 全域(主程式) 字串 str '拿鐵' 字串 str '美式' left right temp 少了第 3 行,第 4 行一執行,拿鐵就沒有任何名字指著,再也找不回來。
實驗室二 ③ 的三個關鍵時刻。temp 的任務只有一個:在 left 被撕下來之前,先替 "拿鐵" 多貼一張標籤。

直接寫 left = right 再 right = left 會失敗,因為第一行執行完,原本 left 指的物件已經沒有名字找得到了。傳統做法是多用一個暫存變數(temporary variable)先留住它。Python 另外提供更簡潔的寫法 left, right = right, left:右邊兩個值先全部找好、暫時打包成一組(元組 tuple,第 5 課),然後才依序貼到左邊的兩個名字上,所以不會互相覆蓋。實務上幾乎都用這一種。

5. 命名規則與好名字

名字有硬性規則,違反就是 SyntaxError,程式一行都不會執行:

  1. 只能用英文字母、數字和底線 _(Python 3 也允許中文等文字,但慣例上用英文)。空格、減號、句點、全形符號都不行。
  2. 不能以數字開頭:cup2 可以,2cup 不行。
  3. 大小寫不同就是不同的名字:price、Price、PRICE 是三個名字。
  4. 不能使用保留字(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 這些內建函式的名字當變數:它們不是保留字,寫了不會馬上出錯,但會把原本的功能蓋掉。

✅ 合法,而且好讀 ❌ 不合法(SyntaxError) ⚠️ 合法,但不建議 unit_price total_cups cup2 MAX_CUPS 2nd_cup my-name total price class x print Price 與 price 價格 snake_case,看得出意思 單字之間用底線 數字不在開頭就可以 全大寫:常數的慣例 數字開頭 減號會被當成減法 空格把它拆成兩個名字 保留字 看不出代表什麼 會蓋掉內建的 print 只差大小寫,容易搞混 能執行,但慣例用英文 先檢查硬性規則(中欄),通過之後再問:別人看得懂嗎?會不會和其他名字撞在一起?
三欄分別對應實驗室三的三個選項。中欄每一個都已經用 Python 3.13 實際執行過,全部是 SyntaxError;右欄都能執行,問題出在可讀性或日後的副作用。

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 callable

7. 兩段完整範例

範例 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