布林值與流程控制

日常生活中,我們無時無刻都在面對許多的選擇,要不要吃飯、要吃什麼、要不要睡覺、要不要出去玩..這些選擇都是由一個又一個的邏輯判斷所組成的。程式語言也是一樣,在這個章節,我們來看看在 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):
print("ok") # 這裡用 3 個空白
print("hey") # 這裡用 2 個空白
這樣寫會得到語法錯誤而無法執行。雖然並沒有強制的規定要用幾個空白來進行縮排,但根據 Python 的官方文件,建議使用 4 個空白來進行縮排,本書的範例也都會使用 4 個空白進行縮排。
回到原本的範例,如果 age 大於等於 18 歲會執行,但如果小於 18 歲呢?你會發現什麼事都不會發生,因為我們只有提到「如果」,沒有「不然」、「否則」...