布林值與流程控制
日常生活中,我們幾乎無時無刻都在面對許多的選擇,要不要吃飯、要吃什麼、要不要睡覺、要不要出去玩..這些選擇都是由一個又一個的邏輯判斷所組成的。程式語言也是一樣,在這個章節,我們來看看在 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):
A | B | A and B | A or B |
---|---|---|---|
True | True | True | True |
True | False | False | True |
False | True | False | True |
False | False | False | False |
另外,跟數學的四則運算一樣,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):
# 用 3 個空白
print("ok")
# 這裡用 2 個空白
print("hey")
這樣寫會得到語法錯誤而無法執行。雖然並沒有強制的規定要用幾個空白來進行縮排,但根據 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 的倍數為 366天閏年。
- 年份為 100 的倍數但非400的倍數為平年。
- 年份為 400 的倍數為閏年。
先想想看什麼叫做「4 的倍數」?以數學的說法就是「能被 4 整除」的數字,也就是說取 4 的餘數等於 0,在上個章節有介紹到取餘數可以用 %
符號來做:
year = 100
if year % 4 == 0:
print(f"{year} 是 4 的倍數")
所以 100 的倍數或是 400 的倍數就是以此類推。我們就用這些規則加上剛學到的 if...else...
來寫一個判斷閏年的程式,一開始如果對這些規則或邏輯判斷還不太熟悉,可以先用紙筆畫個流程圖,像這樣:
再用程式碼來實作,寫的時候先不用管程式碼漂不漂亮,可以正常運作再說:
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 在其他程式語言可是大家搶著抄但又不一定抄的起來的好用設計。