跳至主要内容

布林值與流程控制

布林值與流程控制

日常生活中,我們無時無刻都在面對許多的選擇,要不要吃飯、要吃什麼、要不要睡覺、要不要出去玩..這些選擇都是由一個又一個的邏輯判斷所組成的。程式語言也是一樣,在這個章節,我們來看看在 Python 裡如何進行邏輯判斷,以及使用布林值來進行流程控制。

布林值(Boolean)​

「聽說你們台中人常會說『真的假的』,有這回事嗎?」「有嗎?真的假的?」

根據不可考的統計,好像不少台中人都有「真的假的」的口頭禪,程式語言裡面也有「真的」跟「假的」,而且沒有其他的灰色地帶。真假值,又稱為布林值(Boolean),只有兩種可能的值,常用來表示「對」或「錯」、「真」或「假」、「有」或「沒有」等等之類的非黑即白的二元狀態。

在 Python 的真假值就是 True 跟 False,要注意大小寫,有些程式語言是用小寫的 true 跟 false。其實布林值跟一般的數字或字串一樣就只是程式語言裡面的兩個值而已,只是剛好這兩個值在邏輯判斷上有特別的意義。如果你試著用 type() 函數來檢查布林值的型別,會發現這兩個都是 bool:

>>> type(False)
<class 'bool'>

>>> type(True)
<class 'bool'>

但可能比較多人不知道的,在 Python 的布林值其實也是一種「數字」,我們可以用幾個內建的函數證明給大家看:

# 檢查是不是一種 int
>>> isinstance(True, int)
True

這裡的 isinstance() 函數是用來判斷是不是一種 int,類別以及實體在後面「物件導向」的章節有詳細介紹,現在大家只要先知道 Python 的布林值雖然看起來是 True 跟 False,但在本質上就是數字。正因為布林值也是數字,也就是說,數字型態能做的操作,例如四則運算,布林值也能做。如果遇到型別轉換的話,True 轉換成數字就是 1,False 就是 0,你猜猜看以下這段程式碼會得到什麼結果:

>>> True + True * 2 - False + True

因為布林值本身就是一種數字型態,所以它也支援數字的四則運算,所以上面這段程式碼等同於 1 + 1 x 2 - 0 + 1,結果會得到 4。我知道這看起來有點怪,你現在知道這背後發生什麼事,你可以選擇不要這樣寫。

跟前面提到的 str、int 一樣,雖然你也可以拿 bool 這個名字來當一般變數的名稱,像這樣:

bool = "你好"

可以是可以,但別這樣做,不然不知道什麼時候會砸到自己的腳。

沒了,布林值就這麼簡單。就我觀察,很多軟體工程師程式寫久了,個性也變得像布林值一樣「非黑即白」的二元性格,沒有灰色地帶,我大膽推論也可能跟這有關。

型別轉換​

數字能轉字串、字串能轉數字,所以其他型別也能轉布林值,只是轉換的結果不一定是你所想像的。我們可以使用內建的 bool() 函數來進行型別轉換。在 Python 大部份的布林型別轉換的結果都是 True,雖然我們沒辦法列出所有的真值(Truthy Value),因為幾乎等於有無限多個,但我們可反向的列出有限的幾個假值(Falsy Value),利用排除法,假值以外的值就全部都是真值了。

以下這些值在 Python 在做 bool() 型別轉換時會是 False:

  • False 本身
  • None
  • 數字 0
  • 空字串 ""
  • 空的「容器」 []、()、{}

除此之外的結果都是 True。

另外,剛剛提到布林值其實也是一種數字,所以我們也可以用 int() 或 float() 函數來把布林值轉換成數字:

>>> int(True)
1

>>> int(False)
0

>>> float(True)
1.0

>>> float(False)
0.0

比較的時候...​

在 Python 裡在做數值大於、小於或等於比較的時候,會得到布林值的結果。比如說:

>>> 1 < 2
True

>>> 1 == 2
False

我們在前面介紹變數的時候有提過在 Python 以及大部份的程式語言裡,一個等號 = 通常是「指定」,而兩個等號 == 才是「比較」的意思。附帶一提,Python 沒有三個等號 === 的設計,別把其他程式語言(例如 JavaScript)的習慣帶到 Python 來。

再跟大家分享個不重要的冷知識,Python 的作者 Guido 在設計 Python 之前主要是寫另一個叫做 ABC 的程式語言,在 ABC 這款程式語言裡,比較就真的是用一個等號 =,而不是兩個等號 ==。所以 Guido 一開始在設計 Python 的時候,曾經有考慮過用一個等號來做比較,也就是一個等號可以用來指定也可以用來做比較,但這實在是很容易造成混亂,所以最後還是選擇使用兩個等號來做比較。

當布林值跟數字進行比較的時候,Python 會先把布林值轉換數字才開始進行比較,例如:

>>> 1 == True
True

>>> 1 == 1.0 == True
True

>>> 0 == False
True

正是因為這個特性,你甚至可以把布林值拿來當索引值:

>>> message = "hellokitty"

# 等於索引值 1
>>> message[True]
'e'

# 等於索引值 0
>>> message[False]
'h'

這樣也行,但沒事別這樣寫!

邏輯運算​

布林值常見的用途之一是用來進行邏輯運算。Python 提供了三個關鍵字來進行邏輯運算:and、or、not,光看字面大概就知道是什麼意思了。and 是需要兩個都成立,才是算成立;or 是只要其中有一個成立,就算成立;not 則是把布林值的真假值顛倒過來:

>>> True and True
True

>>> True or False
True

>>> not True
False

簡單的把 and 跟 or 的結果列成一個真值表(Truth Table):

ABA and BA or B
TrueTrueTrueTrue
TrueFalseFalseTrue
FalseTrueFalseTrue
FalseFalseFalseFalse

另外,跟數學的四則運算一樣,Python 的邏輯運算也有優先順序,在沒有小括號的情況下,not 的優先順序最高,其次是 and,最後是 or。

邏輯短路(Short-circuit)​

不少程式語言的邏輯運算都有一種叫做「邏輯短路」(Short-circuit)的設計,這是個有趣的特性,但偶爾也會不小心造成程式錯誤。當 Python 在進行 and 運算的時候,如果第一個值是 False,第二個值不管是 True 或是 False 都不會影響最終結果,因此 Python 就不會再去看第二個值是什麼,直接就在這裡放棄判斷或執行,計算出結果為 False;

# 不會執行後面的 print() 函數
>>> False and print("媽我在這")
False

相對的,當 Python 在進行 or 運算的時候,如果第一個值是 True,接下來不管是 True 或 False 都不會影響結果,同樣也會放棄執行判斷,計算出結果為 True:

# 不會執行後面的 print() 函數
>>> True or print("媽我在這")
True

跟 and 以及 or 的運算有點像的還有 & 以及 | 符號,但它們不是用來做布林值的邏輯運算,而是用來做位元運算(Bitwise Operation)。所謂的位元運算是指對二進位的每一個位元進行運算,例如我用十進位的數字 10(二進位 1010) 跟 12(二進位 1100)進行 & 運算:

    1   0   1   0 (10)
& 1 1 0 0 (12)
------------------
1 0 0 0

經過 & 的運算之後會得到 1000,也就是十進位的 8,實際在 Python 跑一次:

>>> 0b1010 & 0b1100
8

因為 True 跟 False 其實是一種數字,也就是數字 1 跟 0,所以把位元運算用在布林值上的話也會有跟 and 以及 or 類似的效果,不過這就不會造成邏輯短路的效果了:

>>> True & False
False

>>> True | False
True

如果把布林值跟數字放在一起做位元運算,例如 True & 7,這會發生什麼事呢?數字 7 的二進位是 0111,所以 True & 7 會變成 1 & 7,也就是:

    0   0   0   1 (1)
& 0 1 1 1 (7)
------------------
0 0 0 1

答案會得到 1:

>>> True & 7
1

流程控制​

如果...​

最簡單的流程控制,應該就是「如果」了。在 Python 使用 if 關鍵字來進行判斷,寫起來相當直覺、簡單:

age = 20

if age >= 18:
print("你可以投票了")

if 後面接判斷式,後面的冒號 : 別忘了加。這個判斷式可能是比數字大小、比字串長度、判斷布林值是真的還是假的...等等,甚至只放一個數字 0 或字串 "abc" 也會被轉換成布林值。如果判斷式最終的結果是 True 的話,就會執行 if 底下的程式碼,如果是 False 的話,就會跳過這一段程式碼,什麼都不做。在這個範例中 age 是 20,因為 age 大於等於 18 所以比對結果是 True,在 if 區塊裡的 print() 函數就被執行了。在全世界那麼多種程式語言裡,Python 的程式碼算是容易閱讀的,就算不會寫程式,光是用猜也能猜出程式碼的意思。雖然很簡單,但如果程式這樣寫:

age = 20

if (age >= 18):
print("ok")

看起來程式碼沒有什麼差別,只有 print() 函數的位置不太一樣,但執行這段程式碼的時候會發生錯誤:

$ python demo.py
File "/demo.py", line 4
print("ok")
^
IndentationError: expected an indented block after 'if' statement on line 3

這是因為 Python 是藉由縮排(Identation)來判斷程式碼的結構,所以 print() 函數的位置一定要縮排到正確的位置,不然會發生錯誤。正因如此,在 if 區塊裡要縮排而且一定要有東西才行,所以如果你想寫個 if 但目前還沒想到要在 if 裡面寫什麼程式碼(還是就先不要寫就好?),可以使用關鍵字 pass 關鍵字先卡個位置:

if (age >= 18):
pass # 卡位

縮排對 Python 來說是很重要的,如果不遵守的縮排規定的話程式碼會無法執行。其他的程式語言也有縮排的設計,要縮排或不縮排,或想要縮 2 個空格還是 4 個空格都沒人管你,但在 Python 可是管得很嚴的。寫其他程式語言的朋友也許會覺得連縮排都要管未免也管太寬了吧,但當你看過一些程式碼隨便排的專案或是新手的程式碼,說不定就會感謝 Python 在縮排的強制規定。

大家也許都知道縮排對程式碼的可讀性來說滿重要的,但如果連基本的縮排都不做,既然人治管不了,就交給法治,讓 Python 來治治你。在 Python 要用 Tab 鍵、2 個空白或 4 個空白,甚至故意用 3 個空白也沒關係,只要同一個區塊的程式碼有對齊就好。像這樣:

age = 20

if (age >= 18):
print("ok") # 這裡用 3 個空白
print("hey") # 這裡用 2 個空白

這樣寫會得到語法錯誤而無法執行。雖然並沒有強制的規定要用幾個空白來進行縮排,但根據 Python 的官方文件,建議使用 4 個空白來進行縮排,本書的範例也都會使用 4 個空白進行縮排。

回到原本的範例,如果 age 大於等於 18 歲會執行,但如果小於 18 歲呢?你會發現什麼事都不會發生,因為我們只有提到「如果」,沒有「不然」、「否則」...

如果...不然就那樣​

人生總不會只有「如果」,大部份時候都還會有相對應的「不然」、「否則」。在 Python 使用 else 關鍵字來寫這個「否則」:

age = 16

if age >= 18:
print("你可以投票了")
else:
print("你還不能投票")

這樣的程式碼就會根據 age 的值來決定要印出什麼訊息。但難道人生只有 if...else... 兩條路嗎?當然還有,你可以再使用 elif 來增加更多的選擇:

age = 16

if age >= 20:
print("你可以投總統票了")
elif age >= 18:
print("你可以考駕照了")
else:
print("再等幾年")

要注意寫法是 elif 而不是 elseif,這是 Python 的語法。

上面這段範例同樣也是根據 age 的值來決定要印出什麼訊息,但是多了一個 elif 多了一些選擇。一個便當吃不夠可以吃兩個,一個 elif 不夠也可以多加幾個,但 else 應該只會有一個,這個 else 就是上面所有的條件都不符合的時候走的路。

我們來動手寫點程式練練手感吧!

《練習》閏年判斷​

根據維基百科上的記載,閏年的規則是:

  • 年份不是 4 的倍數為平年。
  • 年份為 4 的倍數但不是 100 的倍數為閏年。
  • 年份為 100 的倍數但不是 400 的倍數為平年。
  • 年份為 400 的倍數為閏年。

什麼叫做「4 的倍數」?以數學的說法就是「能被 4 整除」的數字,也就是除以 4 的餘數會等於 0,在上個章節有介紹取餘數可以用 % 符號來做:

year = 100

if year % 4 == 0:
print(f"{year} 是 4 的倍數")

所以 100 的倍數或是 400 的倍數就是依此類推。我們就用這些規則加上剛學到的 if...else... 來寫一個判斷閏年的程式。一開始如果對這些規則或邏輯判斷還不太熟悉,可以先用紙筆畫個流程圖,像這樣:

Leap Year 閏年

再用程式碼來實作,寫的時候先不用管程式碼漂不漂亮,可以正常運作再說:

year = int(input("請輸入年份:"))

if year % 4 == 0:
if year % 100 == 0:
if year % 400 == 0:
print("閏年")
else:
print("平年")
else:
print("閏年")
else:
print("平年")

上面的縮排要小心不要縮錯了,否則不是程式出錯,就是得到不正確的結果。覺得這看起來有點複雜或有點醜嗎?我們拿前面學過的 and 跟 or 來把它簡化一點:

year = int(input("請輸入年份:"))

if year % 4 == 0 and year % 100 != 0 or year % 400 == 0:
print("閏年")
else:
print("平年")

一開始不用強求一口氣就寫出最簡潔的程式碼,先講求不傷身體... 不是,是先講求能夠正常運作,再來慢慢調整。假以時日,你會發現自己也可以寫出越來越簡潔的程式碼。

《練習》剪刀、石頭、布​

這是個從小玩到大的經典的遊戲,我們可以寫個程式跟它玩看看,看誰比較厲害。遊戲規則我就不用特別介紹,直接動手來寫吧:

import random

user_input = int(input("請出拳 1.剪刀 2.石頭 3.布: "))

if user_input == 1 or user_input == 2 or user_input == 3:
# 隨機產生 1~3 的數字,1: 剪刀, 2: 石頭, 3: 布
answer = random.randint(1, 3)

# 判斷輸贏
if user_input == answer:
print("平手!")
elif (user_input == 1 and answer == 3) or (user_input == 2 and answer == 1) or (user_input == 3 and answer == 2):
print("你贏了!")
else:
print("你輸了!")
else:
print("請輸入有效的數字(1、2 或 3)")

這裡使用了 random 模組的 randint() 函數產生 1 到 3 之間的隨機整數,並用數字 1、2、3 分別代表剪刀、石頭、布,這樣判斷起來比較簡單(如果你要用字串或其他方式也行)。上面這種方式是認真的比對玩家跟電腦的出拳結果,但也可看看以下這種偷吃步的寫法:

import random

user_input = int(input("請出拳 1.剪刀 2.石頭 3.布: "))
answer = random.randint(1, 3)

if user_input == 1 or user_input == 2 or user_input == 3:
if answer == 1:
print("平手!")
elif answer == 2:
print("你贏了!")
else:
print("你輸了!")
else:
print("請輸入有效的數字(1、2 或 3)")

仔細看就會發現,這種寫法完全不在乎玩家出什麼拳,反正就是隨機給玩家三種結果的其中一種,好像有點怪,但以結果來說猜拳不就是三種結果其中一個嗎?這種寫法並不是我原本題目想要的練習,只是趁這個機會提一下,解決問題的方法很多時候不只一種。

switch 語法?​

假設你曾經寫過其他的程式語言,或是在學校上過程式設計的課,當遇到一大串連續的 elif 的時候,教課書上都會說應該使用 switch 或 case...when... 之類的語法,看起來比較有結構。

不過在 Python 並沒有這樣的設計,遇到一堆判斷就是用 if...elif...else... 寫就是了。這是因為 Python 的作者認為 switch 語法並不是那麼的必要,if...elif...else... 就能達到相同的效果,寫起來也不是多難的事,所以就沒有加入 switch 語法的設計。沒有 switch 或 case 的設計並沒什麼大不了的,事實上有些程式語言就算有 switch 或 case 的寫法,本質上也只是 if...else... 而已。

不過沒關係,待會我們就會看到一個叫做 match 的設計,這比其他程式語言的 switch 還要香很多!

連結

為什麼 Python 沒有 switch 語法 https://5xcamp.us/why-no-switch

三元運算子​

如果 if...else... 的內容很簡單的時候,有些程式語言都有「三元運算子(Ternary Operator)」的寫法,我以 JavaScript 為例,看起來大概像這樣:

const age = 20
const message = age >= 18 ? "剛~滿~ 18 歲~" : "未成年"

console.log(message)

三元運算子的「三元」指的是 ?、:、= 這三個符號,用三元運算子來取代簡單的 if...else... 流程,語法可能可以更簡潔一點。在 Python 也有類似的設計,但寫法不太一樣,如果我把上面的範例用 Python 改寫的話:

age = 20
message = "剛~滿~ 18 歲~" if age >= 18 else "未成年"

print(message)

看起來有比較簡潔或是更容易理解嗎?也許有,也許沒有,這會因為每個人對程式語法的熟悉程度不同而有所不同。這種寫法只是個選項,並不是遇到 if...else... 就一定要用三元運算子的寫法替換。如果你覺得這樣的寫法比較好,那就用它,如果你覺得還是原本的 if...else... 標準寫法比較熟悉、對味,就用原本的寫法就好。

如果要做到類似的事,也可以利用前面學過的邏輯短路的特性來達成:

age = 20
message = age >= 18 and "剛~滿~ 18 歲~" or "未成年"

print(message)

另外還有一種可能不是那麼容易一下子就看懂的寫法:

age = 20
message = ("未成年", "剛~滿~ 18 歲~")[age >= 18]

print(message)

這裡利用了 age >= 18 的判斷會得到 True 或 False,前面我們也提過 True 和 False 在 Python 裡其實代表的是 1 和 0,利用這個特點可以把它當做後面章節才會介紹到的 Tuple 或串列的索引值然後得到答案。程式碼雖然看起來比較短也有點酷,但我並不建議這樣寫。

match 比對​

前面提到 Python 並沒有其他程式語言的 switch 語法,不少人在敲碗但碗都敲破了也沒有用,在 PEP 上的提議一直被否決,就覺得不太需要。不過在許多人的努力下,終於在 PEP 634 定案,並且 Python 3.10 實作了新的語法 match,寫起來的手感跟其他程式語言的 switch 語法有點像。match 語法的正式名稱叫做「結構化模式比對(Structural Pattern Matching)」,由於是在 3.10 版之後才有,所以太舊版本的 Python 沒有支援這種寫法。

if...else...的替代品​

假設我有原本個簡單流程判斷像這樣:

weather = "rain"

if weather == "rain":
print("下雨天")
elif weather == "sunny":
print("出太陽")
elif weather == "cloudy":
print("多雲")
else:
print("不知道")

使用 match 語法的話,可以寫成這樣:

weather = "rain"

match weather:
case "rain":
print("下雨天")
case "sunny":
print("出太陽")
case "cloudy":
print("多雲")
case something:
print(f"我不知道 {something} 是什麼天氣")

前面三個 case 分別是在比對字串(Python 會使用 == 來進行比對),如果比對成功的話就會結束整個比對的過程。最後一個 case 是指如果上面所有的路線都比對失敗的話會走的路,差不多就是 else 的概念,在後面加上變數名稱的話表示比對到的值會被指派給這個變數。上面我用了 something 做為這個變數的名字,但要用什麼名字你喜歡就好,如果發現在這個 case 裡根本沒打算使用這個變數或是懶得想名字的話,可以給它一個底線 _:

weather = "rain"

match weather:
case "rain":
print("下雨天")
case "sunny":
print("出太陽")
case "cloudy":
print("多雲")
case _:
print("我根本不在乎是什麼天氣")

那個 _ 代表「I don't care」的意思。

看到這裡,大家有覺得這樣語法看起來有比較清楚嗎?老實說我認為並沒有差多少。如果只是這樣的簡單比對的話,我也認同用 if...else... 還比較直覺一點。但如果你以為 match 語法就只有這樣,那你就太小看 Pattern Matching 了,Pattern Matching 在其他程式語言可是大家搶著抄但又不一定抄的起來的好用設計。

比對型別​

舉個例子,假設我想判斷某個值是數字還是字串,用 if...else... 寫起來大概像這樣:

value = True

if type(value) in [int, float]:
print("數字")
elif type(value) == str:
print("字串")
else:
print("我不知道你是誰")

match 不只能比對簡單的值,還能比對型別,改用 match 寫的話:

data = 123

match data:
case int() | float():
print("數字")
case str():
print("字串")
case _:
print("其他型別")

在第一個 case 中間的那個 | 是「或者」的意思,雖然整個行數沒有變比較少,但程式碼的可讀性好像有好那麼一點點。

比對結構​

match 還能用來比對串列、Tuple、字典、集合等等資料結構,在處理比較複雜的資料結構時挺方便的:

user_data = [1, "悟空", 18]

match user_data:
case [id, name, age]:
print(f"{id} - 我是 {name},我今年 {age} 歲")

case _:
print("你好")

在 user_data 裡面有三個值,透過 Pattern Matching 如果有「比對」到一樣的模式,被比對到的部份可以直接變成變數。也許你並不需要所有的變數,例如我們只想要得到資料裡的第一個值,剩下的不需要,可以用底線 _ 來比對不需要的部份:

user_data = [1, "悟空", 18]

match user_data:
case [id, _, _]:
print(f"會員編號:{id}")

case _:
print("hi")

這應該就不是一般的 if...else... 或其他程式語言的 switch 能夠輕鬆做到的事了。除了串列、Tuple 能比對,字典也能比對:

user_data = {"name": "Kitty", "gender": "female"}

match user_data:
case {"gender": "male", "name": name}:
print(f"{name} 先生您好!")

case {"gender": "female", "name": name}:
print(f"{name} 女士您好!")

case _:
print("您好!")

原本一般會使用 if...else... 來判斷 gender 的值再決定怎麼打招呼,透過 Pattern Matching 可以直接比對到字典裡 gender 的值,而且順序還不需要一樣,這樣的寫法看起來更直覺。

Python 的 match 不只這樣,還能在 case 後面加上條件,像這樣:

number = [10, 0]

match number:
case x, y if y != 0:
print(x / y)

case _:
print("第二個數字不能為零!")

另外,match 可以使用 * 符號一次比對多個,像這樣:

numbers = [1, 2, 3, 4, 5]

match numbers:
case _, *others:
print(others)

case _:
print("Hello Python")

第一個元素我刻意使用 _ 表示我不需要,但後面的元素可以使用 * 符號把它全部收下來,這個應該也是一般程式語言的 switch 辦不到的事。

講了這麼多關於 match 的介紹,最後用一個簡單但算經典的例子來做個收尾,大家不知道有沒有聽過「費氏數列(Fibonacci)」?這個數列的規則是:

  • 第 1 項是 0
  • 第 2 項是 1
  • 第 3 項開始每一項都是前兩項的和,所以這個數列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... 依此類推。

這東西跟我們的年夜飯的概念有點像,大年初三吃的就是大年初一加上初二的總合。這個數列是很經典的「遞迴函數」的實作,用 match 可以寫的很簡潔易懂:

def fib(n):
match n:
case n if n < 2:
return n
case _:
return fib(n - 1) + fib(n - 2)

print(fib(6)) # 第 6 項 = 8
print(fib(8)) # 第 8 項 = 21

這裡有些像是函數或回傳值之類的內容是後面的章節才會介紹到,我這裡只是想透過這個例子讓大家感受一下 match 的好用之處,它不像其程式語言的 switch 只是 if...else... 的變形或替代品,它還能用來比對或拆解更複雜的資料結構,在適當的地方使用 match 可以讓程式碼看起來更直覺、更易懂。

虛無飄渺的 None​

什麼是「存在」?什麼又是「不存在」?你是否有想過,如果某個東西不存在,你是什麼時候知道它不存在的,或是怎麼知道它不存在的?

在寫程式的時候,有時候我們會需要一個「不存在」或「未定義」的值,大部份的程式語言都有類似的設計,例如 NULL、nil 或 undefined 之類的值。在 Python 裡,這個不存在的值叫做 None。

跟布林值的 True、False 一樣,None 的第一個字也是大寫。布林型態還有兩種值,但 None 就只有一個。None 不是 0、空字串、空串列、空字典或空 Tuple,它就只是一個代表「沒有」或「還沒設定」的值,它是一個特殊的資料型別,叫做 NoneType:

girl_friend = None
print(type(girl_friend)) # 印出 <class 'NoneType'>

這個 NoneType 型別就只有它一個值,自成一派。

所以,回頭想想 None 是存在的嗎?是的,它是一個實實在在存在的值,只是它用來代表目前沒有值而已。

== 與 is​

很多程式語言會使用 2 個等號 == 比較兩個值是否相等,Python 也不例外,但 Python 還有個特別的比較方法 is,它可以用來比較是不是同一顆物件:

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True
>>> a is b
False

雖然變數 a 跟 b 看起來都是 [1, 2, 3],但實際上它們在 Python 的世界是兩個不同的東西。你可以想像買了兩盒一樣的玩具,如果沒有特別的碰撞或破損的話,這兩盒玩具的包裝盒看起來都一樣,裡面的玩具看起來也是一樣,但這兩個玩具本質上就是兩個不同的玩具。或是像雙胞胎兄弟姐妹一樣,就算外表或打扮穿著看起來再怎麼像,還是兩個不同的人。在 Python 有個內建函數 id(),可以用來查看某個東西在 Python 世界裡的「編號」:

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> id(a)
4303554880
>>> id(b)
4303556672

可以看的出來這兩個串列雖然看起來一樣,但內部的編號是不同的。再來給大家看一個例子:

>>> a = [1, 2, 3]
>>> b = a
>>> a == b
True
>>> a is b
True

第二行的 b = a 的意思,就是把 a 的值指派給變數 b,所以這時候不管是 a 或 b 都是指向同一個地方,它們的 id() 函數的執行結果也會一樣,所以 a is b 會得到 True。

也就是說,其實 a is b 跟 id(a) == id(b) 是一樣的意思,只是 is 這個運算子比較簡單一些。到這裡應該還算容易理解,接著來看比較難一點的:

a = "hellokitty"
b = "hellokitty"
print(a == b) # 印出什麼?
print(a is b) # 印出什麼?

猜猜看,你覺得會印出什麼結果?

a == b 比較容易猜,就是比對這兩個變數的值是不是一樣,所以會得到 True。而 is 會比較這兩個 "hellokitty" 字串是不是同一個,這在其他的程式語言可能會有不同的結果,我用 Ruby 舉個例子:

>> a = "hellokitty"
>> b = "hellokitty"
>> a.object_id
=> 47540
>> b.object_id
=> 56940

這裡的 object_id 方法跟剛剛提到的 id() 函數的功能差不多,在 Ruby 裡的字串雖然看起來一樣,但它們實際上在記憶體裡就是兩個不同的東西。這在 Python 裡會有不同的結果,還記得我們在上個章節才講到 Python 的字串是不可變(Immutable)的嗎?既然不能變,所以 Python 內部會建立一個「表格」用來存放一些字串,試著盡可能的把同樣的字串存在同一個地方,這樣可以節省一些記憶體空間,這個機制叫做「字串駐留(String Interning)」。當你試著把一個字串指定給某個變數的時候,Python 會決定要不要使用之前已經使用過的字串,也就是去那個表格裡查看看有沒有相同的字串,所以如果用 id() 函數查一下 "hellokitty" 字串:

>>> id("hellokitty")
4305320112
>>> id("hellokitty")
4305320112
>>> id("hellokitty")
4305320112

會發現每次都是一樣的。但並不是所有的字串都會發動這個機制,例如:

>>> id("hello kitty")
4305197104
>>> id("hello kitty")
4305197424
>>> id("hello kitty")
4305195568

String Interning 機制只會發生在幾個情況,當字串的字數比較少而且只包含英文字母、數字、底線的時候,Python 才會發動這個機制,而 "hello kitty" 字串裡帶有空白字元,Python 就沒有幫我們把它存在剛剛提到的表格裡,而是每次都會產生一個新的字串。如果你想要強制讓 Python 發動 String Interning 機制的話,可使用內建模組 sys 的 intern() 函數:

>>> import sys
>>> a = sys.intern("hello kitty")
>>> b = sys.intern("hello kitty")
>>> a is b
True

不只字串,數字也有類似的機制,但這個機制不會無限上綱到所有的數字,只有在 -5 到 256 之間的整數才會發生:

>>> a = 256
>>> b = 256
>>> a is b
True

# 超過範圍了
>>> a = 257
>>> b = 257
>>> a is b
False

為什麼只設定在這個範而已?因為 Python 的核心團隊認為 -5 到 256 之間的數字很常使用,為了效能考量,Python 把這些數字直接寫在直譯器裡,等於是在程式啟動的時候就先幫我們把這些數字先準備好,如果在我們的程式有用到這個範圍的數字的話就直接拿去用,不用另外重新產生一份。

你是不是 None?​

在比較某個變數是不是 None 的時候有幾種寫法,一種是使用兩個等號:

girl_friend = None

if girl_friend == None:
print("單身")

這沒什麼問題,但 Python 官方建議不要使用兩個等號 == 運算子,而是改用 is 來比對:

girl_friend = None

if girl_friend is None:
print("單身")

原因之一,是 None 跟布林值一樣,在 Python 只有存在一份,所以只要是 None,不管是你的 None 還是我的 None 都是一樣的,用 id() 函數算出來的結果都同一顆。

原因之二,兩個等號 ==實際上的運作方式,會呼叫物件身上的 __eq__() 方法,而這個方法是有機會被修改的,例如:

class Human:
def __init__(self, name):
self.name = name

def __eq__(self, value):
return True

如果上面這段語法看不懂沒關係,在物件導向的章節會再詳細說明。簡單的說,這裡我讓 __eq__() 方法永遠回傳 True,在用兩個等號做比較的時候,這個方法會被執行,所以不管你比對的是什麼,都會得到 True:

>>> girl_friend = Human("新垣結衣")
>>> girl_friend == None
True

而 is 是 Python 的保留字沒辦法被改,所以在比對 None 的時候,建議使用 is None 或 is not None 的寫法。

工商服務

想學 Python 嗎?我教你啊 :)

想要成為軟體工程師嗎?這不是條輕鬆的路,除了興趣之外,還需要足夠的決心、設定目標並持續學習,我們的ASTROCamp 軟體工程師培訓營提供專業的前後端課程培訓,幫助你在最短時間內建立正確且扎實的軟體開發技能,有興趣而且不怕吃苦的話不妨來試試看!