約維安計畫:Python 的流程控制

第十五週。

什麼是流程控制

多數程式語言都會從程式碼的第一列開始按照列(Row-wise)的順序往下讀取並且執行,但是在某些情況下,我們會希望能在某些程式碼可以依照需求不執行、執行或者執行多次,這時就可以透過流程控制的機制來滿足這些情況。

流程控制指的是在程式執行時,能夠依賴條件來決定程式區塊(Code blocks)中的指令、敘述或者函數呼叫的執行次數,在條件為布林 False 的時候,程式區塊中的程式碼執行次數為 0(不執行);在條件為布林 True 的時候,程式區塊中的程式碼執行次數大於等於 1。具體來說,流程控制是一種機制,這個機制能讓一段指令在某些情況下不被執行、在某些情況下被執行一次或者在某些情況下被重複執行,Python 實現流程控制有三種方式:

  1. 條件判斷。

  2. 迴圈。

  3. 例外處理。

其中,條件判斷能夠使用 if-elif-else 敘述與布林完成;迴圈則有兩種選擇,一是使用 while 敘述與布林,二是使用 for 敘述與可走訪物件(Iterables);例外處理能夠使用 try-except 敘述與錯誤種類完成。

什麼是區塊

區塊(Code blocks)指的是將一系列相關的敘述集合在一起的程式碼結構,在多數的程式語言中,區塊使用一對大括號(A curly-brace pair)將敘述包裝在裡面形成區塊。但是,在 Python 中建立區塊跟多數的程式語言截然不同,Python 使用縮排(Indentations)而非一對大括號來將敘述包裝為區塊,在前述三種類型的流程控制中,都會利用區塊將敘述依附給條件判斷、迴圈與例外處理,如此一來就能夠在某些情況下讓區塊中的程式不被執行、執行僅一次或執行特定次數。

條件判斷

條件判斷是依指定變數或者運算的布林值為真或假時,來決定是否執行一段位於某區塊內的程式,透過 if-elif-else 敘述可以根據指定條件是否成立,決定後續要執行的程式,也可以組合多個 if-elif-else 敘述進行更複雜的條件判斷。

我們使用保留字 if 搭配一個條件(之後接一個冒號 :)就能夠建立一個 if 敘述,if 敘述能夠讓區塊的執行與否依賴條件的布林值,如果條件為 True 則執行依附其縮排下的程式區塊;若條件為 False 則不執行依附其縮排下的程式區塊。

if CONDITION:
    # code block attached to if statement

CONDITION 可以透過關係運算符或者邏輯運算符生成,例如判斷輸入整數是否為正。

def is_positive(x):
    if x >= 0:
        return f"{x} is positive."
    if x < 0:
        return f"{x} is negative."

print(is_positive(0))
print(is_positive(-1))

我們使用保留字 if 與 elif 搭配各自的條件建立一個 if-elif 敘述,如果 if 後面的條件為 True 則執行依附其縮排下的程式區塊;若 if 後面的條件為 False 則跳過該區塊改為檢視 elif 後面的條件,如果是 True 就執行依附其縮排下的程式區塊;若 elif 後面的條件為 False 則跳過該區塊,其中 elif 是 else if 的簡寫。

if CONDITION:
    # code block attached to if statement
elif CONDITION:
    # code block attached to elif statement

CONDITION 可以透過關係運算符或者邏輯運算符生成,例如判斷輸入整數是否為正。

def is_positive(x):
    if x >= 0:
        return f"{x} is positive."
    elif x < 0:
        return f"{x} is negative."

print(is_positive(0))
print(is_positive(-1))

我們使用保留字 if 、else 搭配一個條件建立一個 if-else 敘述,如果 if 後面的條件為 True 則執行依附其縮排下的程式區塊;若 if 後面的條件為 False 則跳過該區塊改為執行依附 else 縮排下的程式區塊。

if CONDITION:
    # code block attached to if statement
else:
    # code block attached to else statement

CONDITION 可以透過關係運算符或者邏輯運算符生成,例如判斷輸入整數是否為正。

def is_positive(x):
    if x >= 0:
        return f"{x} is positive."
    else:
        return f"{x} is negative."

print(is_positive(0))
print(is_positive(-1))

在前面的例子中,我們可以看到同樣一個「判斷輸入整數是否為正」的函數,可以用不同的條件判斷敘述來實作,那麼在應用情境上,該如何區別與採用?簡單來說,我們可以藉由判斷 CONDITION 是否互斥(Mutually exclusive)來決定。這個說法是依據 if 敘述、if-elifif-else 敘述的主要差別,由於 if-elifif-else 敘述的條件判斷是有先後順序的,當前面 if 敘述得到了布林值 True,意味著後續的 elifelse 敘述就無關緊要了。簡單來說,假如條件彼此之間是互斥的,條件判斷怎麼寫大概都沒有問題;但反過來說,假如條件彼此之間是「非互斥」的話,判斷的順序以及 if 敘述、if-elifif-else 敘述就會需要謹慎地選擇。舉例來說,我們依據 https://en.wikipedia.org/wiki/Body_mass_index 寫作一個函數 get_bmi_desc() 可以根據輸入的 BMI 數值,給予過輕、正常、過重或肥胖的文字輸出,將四個條件寫成互斥的格式:

  1. 過輕:BMI < 18.5。

  2. 正常:BMI >= 18.5 and BMI < 25。

  3. 過重:BMI >= 25 and BMI < 30。

  4. 肥胖:BMI >= 30。

在條件互斥的狀況下,怎麼寫大概都沒有問題。

def get_bmi_desc(bmi):
    if bmi < 18.5:
        bmi_desc = "過輕"
    elif bmi >= 30:
        bmi_desc = "肥胖"
    elif bmi >= 25 and bmi < 30:
        bmi_desc = "過重"
    elif bmi >= 18.5 and bmi < 25:
        bmi_desc = "正常"
    return bmi_desc

print(get_bmi_desc(17))
print(get_bmi_desc(23))
print(get_bmi_desc(29))
print(get_bmi_desc(31))

但這不表示條件判斷就必須將條件都寫成互斥才可以,在適當順序與敘述安排下,即使非互斥也能達到正確判斷結果。

def get_bmi_desc(bmi):
    if bmi >= 30:
        bmi_desc = "肥胖"
    elif bmi >= 25:
        bmi_desc = "過重"
    elif bmi >= 18.5:
        bmi_desc = "正常"
    else:
        bmi_desc = "過輕"
    return bmi_desc

print(get_bmi_desc(17))
print(get_bmi_desc(23))
print(get_bmi_desc(29))
print(get_bmi_desc(31))

迴圈

迴圈是常見的流程控制之一,這是一種在區塊中只出現一次、但卻可能被連續執行多次的程式碼結構,區塊中的程式碼會執行特定的次數、執行到特定條件成立時結束或者走訪資料結構中的所有內容,與串列切割的語法概念相似,迴圈也能夠用開始、結束與間隔三個要素描述。

  • start:迴圈的開始。

  • stop:迴圈的結束。

  • step:迴圈從起始前往終止的方法。

我們可以使用保留字 while 與條件建立一個 while 迴圈區塊,當條件為 True 重複執行依附其縮排下的程式區塊,描述迴圈的 step 常用複合運算符(Compound operators),例如 i += step 就等同於 i = i + step

i = 0 # start
while CONDITION: # stop
    # code block attached to while statement
    i += step

CONDITION 可以透過關係運算符或者邏輯運算符生成,例如印出前 n 個奇數。

def print_first_n_odds(n):
    i = 1
    odds = []
    while len(odds) < n:
        print(i)
        odds.append(i)
        i += 2

print_first_n_odds(5)

我們可以使用保留字 for 與可走訪物件(Iterables)建立一個 for 迴圈區塊,依附其縮排下的程式區塊會重複執行,直到可走訪物件中的所有元素都被走訪完畢。

for i in ITERABLE: # start/stop/step
    # code block attached to for statement

可走訪物件被宣告完成的當下,迴圈的三要素就已經描述完畢。

  • start 可迭代物件的第 0 項元素。

  • stop 可迭代物件的最後一項(索引值 -1 的元素)。

  • step 可迭代物件的逗號區隔。

ITERABLE 可以內建函數 range() 生成,例如印出前 n 個奇數。

help(range)
def print_first_n_odds(n):
    iterable = range(1, n*2, 2)
    for i in iterable:
        print(i)

print_first_n_odds(5)

在前面的例子中,我們可以看到同樣一個「印出前 n 個奇數」的函數,可以用不同的迴圈類型來實作,那麼在應用情境上,該如何區別與採用?簡單來說,我們可以藉由判斷區塊程式「重複執行的次數是否已知」來決定,已知重複執行次數的情境可以用任意迴圈,未知重複執行次數的情境僅能採用 while 迴圈。這個說法是依據 while 迴圈與 for 迴圈的主要差別,while 在重複執行區塊程式的時候,會在 CONDITIONFalse 的時候終止;而 for 在重複執行區塊程式的時候,會在可迭代物件中的元素用罄時候終止。這表示 while 迴圈根據一個型別為布林變數的物件決定重複執行程式區塊是否終止;for 迴圈則根據一個已知元素數量的可迭代物件決定重複執行程式區塊是否終止。舉例來說,內建函數 bin() 可以將十進位整數轉換為二進位文字,轉換方式是透過「輾轉相除法」將十進位整數持續以 2 相除,直到商數的部分為 0 或者 1,其中「輾轉相除法」就是一個「未知重複執行次數」的情況,沒有辦法在一開始就得知需要多少次的輾轉相除才能結束。

def int_to_bin_str(x):
    if x < 2:
        return str(x)
    binary_str = ""
    while x > 0:
        modulo = x % 2
        binary_str = str(modulo) + binary_str
        x //= 2
    return binary_str

print(int_to_bin_str(2))
print(int_to_bin_str(3))

在 Python 中所謂的可走訪物件(Iterables)是任意長度的資料結構(包含串列、Tuples、集合、字典)或者文字,這些可走訪物件都能夠直接放置在 for 迴圈中使用。

def print_iterable_elements(x):
    for e in x:
        print(e)

primes_list = [2, 3, 5, 7, 11]
primes_tuple = (2, 3, 5, 7, 11)
primes_set = {2, 3, 5, 7, 11}
primes_str = "2357"
print_iterable_elements(primes_list)
print_iterable_elements(primes_tuple)
print_iterable_elements(primes_set)
print_iterable_elements(primes_str)

但字典是具有鍵(keys)與資料(values)的資料結構,在走時要特別留意是希望走訪字典的鍵或者字典的值,透過字典的 keys() 、 values() 與 items() 方法分別可以取出鍵、資料或者兩者,其中 items() 方法會將標籤以及資料兩兩合併在一個 Tuple 中。

primes_dict = {
    "1st": 2,
    "2nd": 3,
    "3rd": 5,
    "4th": 7,
    "5th": 11
}
# Iterating a dict
for k in primes_dict.keys():
    print(k)
for v in primes_dict.values():
    print(v)
for k, v in primes_dict.items():
    print("{} is the {} prime.".format(v, k))

將流程控制的兩個技巧條件判斷與迴圈合併使用會讓我們的程式撰寫更有彈性,保留字 break 能夠讓我們在走訪的過程中在條件為 True 的時候終止迴圈,舉例來說當走訪到非整數的時候終止迴圈。

primes = [2, 3, "5", 7, 11]
for p in primes:
    if not isinstance(p, int):
        break
    print(p)

保留字 continue 能夠讓我們在走訪的過程中在條件為 True 的時候略過該元素但是繼續完成迴圈,舉例來說當走訪到非整數的時候略過。

primes = [2, 3, "5", 7, 11]
for p in primes:
    if not isinstance(p, int):
        continue
    print(p)

例外處理

例外處理指的是針對程式區塊執行時所出現的例外(Exceptions)進行對應的處置,在執行程式區塊時遭遇的例外往往是由於錯誤而引發,包含無效的物件名稱、檔案不存在或型別不正確等,錯誤會無條件停止 Python 程式,破壞程式執行正常的流程,例外處理就是在程式區塊遭遇例外時能夠移交控制權的一種機制。簡單來說,例外處理就像是另一種類型的條件判斷,只不過條件判斷倚賴條件的布林值決定哪個程式區塊要被執行,而例外處理則是依賴程式區塊是否產生了錯誤來決定哪個程式區塊要被執行。

我們可以使用保留字 tryexcept 與錯誤類別建立一個例外處理區塊,依附 try 保留字縮排下的程式區塊一但執行時遭遇到了錯誤,就會跳過該區塊改為執行依附 except 保留字縮排下的程式區塊。

try:
    # code block attached to try statement
except:
    # code block attached to except statement

例如將輸入的兩數相除,在輸入兩數其中一者為文字時候會產生型別錯誤(TypeError)而導致 Python 程式執行停止,也因此在呼叫 divide() 函數後的 print() 函數都沒有被執行。

def divide(a, b):
    return a / b

print(divide(55, '0'))
print("This message will not be printed.")
print("This message will also not be printed.")

加入例外處理,可以確保在呼叫 divide() 函數後的 print() 函數都會被執行。

def divide(a, b):
    try:
        return a / b
    except:
        return None

print(divide(55, '0'))
print("This message will be printed.")
print("This message will also be printed.")

若在保留字 except 之後加上錯誤類別可以依照產生的錯誤,回傳不同的對應輸出。

def divide(a, b):
    try:
        return a / b
    except TypeError:
        return "TypeError occurred."
    except ZeroDivisionError:
        return "ZeroDivisionError occured."

print(divide(55, '0'))
print(divide(55, 0))
print("This message will be printed.")
print("This message will also be printed.")

在認識了條件判斷、迴圈與例外處理這三種流程控制的機制之後,第十五週約維安計畫:Python 的流程控制來到尾聲,希望您也和我一樣期待下一篇文章。

計畫學員專區

約維安計畫學員可以點選 nbgitpuller 連結將本篇文章完整的 Jupyter Notebook 複製到自己的 JupyterHub 之中:

nbgitpuller 連結

伺服器選項選擇預設的「Data science environment:學習 Python、R 與 Julia 的資料科學環境」就可以順利運作文章中的範例程式。

延伸閱讀

  1. https://docs.python.org/3/tutorial/errors.html


對於這篇文章有什麼想法呢?喜歡😻、留言🙋‍♂️或者分享🙌

Leave a comment