Try   HackMD

Python Programming - Lecture 04 迴圈

tags: python-programming

S M T W T F S Back to the beginning. r-906 — 《パノプティコン》

這就是你,社會的小螺絲釘,跟隨人工定義出來的星期制按表操課,日覆一日,直到你屆齡退休為止。

但今天我們沒有要學習社會學,我們只是要學習迴圈而已。所以比起開始質疑結構功能論是否只是資產階級用來奴役勞工的託詞,我們不如來試試如何用 Python 語法來表達一個普通上班族的生活模式:

while age < retirement_age:
    for day in ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']:
        if day == 'Saturday' or day == 'Sunday':
            take_a_day_off()
        else:
            work()
    # age increases every 365 days

哇,突然多了很多沒看過的語法,好像看得懂又好像有點看不懂這段程式在幹嘛?沒關係,現在先不論語法如何正確撰寫,這段程式碼的意思可以簡單地理解如下:

  • 若我的年紀還小於退休年紀,持續做以下動作:
    • 對於星期日、星期一、星期二、星期三、星期四、星期五、星期六當中的每一天,做以下動作:
      • 如果今天是禮拜六,或是今天是禮拜天,則:
        • 休息一天
      • 否則:
        • 工作

在這裡,我們首次看到了程式碼可以重複進行同樣動作的行為,而這個「重複執行」就是迴圈最核心的概念。重複執行可以有兩種不同的重複方法,第一種是「若某條件成立,則一直重複做某件事」,以上方程式範例來說,「若我的年紀還小於退休年紀,持續做以下動作」就是屬於這一種迴圈,其中「我的年紀還小於退休年紀」的部份就是迴圈繼續執行的條件;第二種是「對於某個範圍內的每一個東西,都做一樣的事情」,以上方程式範例來說,「對於星期日、星期一、星期二、星期三、星期四、星期五、星期六當中的每一天,做以下動作」就屬於這一種迴圈,其中迴圈執行的範圍是一個星期當中的每一天。

迴圈是自動化流程當中一個相當核心的元素。之前我們已經提及數次,電腦程式被發明出來的用意就是在於自動化。為什麼會需要自動化呢?就是因為有一些工作真的太枯燥乏味,日復一日地做下去,沒完沒了,讓負責做這個工作的人受不了了。因此才會希望讓沒有情感也不會疲乏的機器來代替人類做這些無聊又重複的事,讓小螺絲釘的生活可以稍微更有品質一點。既然如此,能夠重複執行同樣的動作便是電腦程式必須俱備的能力了,而這樣的功能就是透過迴圈來實作。在本章當中,我們將要來學習兩種不同的迴圈語法,以及如何使用迴圈來進行更加複雜的流程控制。

4.1 while 迴圈

while 迴圈,或是 while 語句,用來表達「若某條件成立,則一直重複進行某動作(,直到該條件不再成立)」的語意。while 語句的語法如下:

>>> while conditional_expression:
...     statement

這個語句的組成內容有:

  • 關鍵字 while
  • 條件表達式 conditional_expression
  • 冒號 :
  • 縮排
  • 一到多句的運算動作 statement

嗯?怎麼有一種似曾相識的感覺?這基本上就只是把 3.2.1 當中提到的 if 語句拿來複製貼上,然後把 if 改成 while 而已嘛!尼是不是在偷懶 R ヽ(#`Д´)ノ

沒錯, while 迴圈和 if 語句的語法基本上是一模一樣的。這兩個語句的差別就只在於: if 語句在條件成立的情況下,只會執行他的程式主體一次;但 while 語句在條件成立的情況下,會不斷重複執行他的程式主體,直到條件不成立為止。具體來說,while 迴圈的運作規則是這樣的:當程式執行到 while 這一行的時候,會檢查 conditional_expression 是否成立。若不成立,則 statement 就不會被執行;若成立,則會執行 statement 一次,然後再回到 while 這一行再檢查一次 conditional_expression。若不成立,則 statement 就不會被執行;若成立,則會執行 statement 一次,然後再回到 while 這一行

while 語句的運作規則中,我們可以觀察到: conditional_expression 的狀態是影響 while 迴圈有沒有辦法停下來的關鍵。當 conditional_expression 不成立 時,迴圈就會停下來。因此,我們可以說 conditional_expression 不成立的情況就是 while 迴圈的中止條件。換句話說,如果 conditional_expression 永遠都會成立,迴圈就永遠都達不到中止條件,那麼這個迴圈就會無止盡的一直執行下去,我們就稱這樣的迴圈是無窮迴圈 (infinite loop)

4.1.1 中止條件

在我們剛剛描述的 while 迴圈的運作規則中,你會發現到程式如果執行進入了 while 迴圈的程式主體,就好像掉進了一個洞裏面,如果不做點什麼的話,就會一直卡在洞裡出不來。要從 while 迴圈的洞裡出來的關鍵,就在於 conditional_expression 要有辦法從成立變成不成立。若你希望你的 while 迴圈在做完他該做的事之後就要停下來,讓程式可以繼續往後執行的話,那你就勢必得在迴圈主體裏面做點什麼,讓 conditional_expression 的狀態可以改變。

舉例來說,如果我們今天要模擬一個倒數計時器,倒數計時器的規則可以這麼描述:「若計時器還沒歸零,印出現在的剩餘秒數」。若只是照著這句話撰寫程式,那麼你寫出來的程式可能會長這樣:

time_remaining = 10 # 還剩 10 秒

while time_remaining > 0:
    print(time_remaining)

上述程式碼正確描述了「若計時器還沒歸零,印出現在的剩餘秒數」的邏輯,讓我們實際執行看看這段程式碼看會發生什麼事:

>>> time_remaining = 10
>>> while time_remaining > 0:
...     print(time_remaining)
... 
10
10
10
10
10
10
10
10
10
10
10
(下方省略上千萬行)

糟糕,程式不但沒倒數,只是一直重複印出 10 之外,現在還停不下來了。別慌張,快按下 Ctrl + C 就可以強迫你的程式停下來了:

10
10
10
10
10
^CTraceback (most recent call last):
  File "<stdin>", line 2, in <module>
KeyboardInterrupt
>>> 

為什麼會這樣呢?我們不是正確描述了倒數計時器的運作了嗎?這是因為當我們說「若計時器還沒歸零,印出現在的剩餘秒數」這句話時,我們省略了一個行為沒有描述,那就是「剩餘秒數會減少」。在現實生活當中,就算你什麼都沒做,時間還是毫不留情的一分一秒逐漸流失。但是在寫程式的時候,如果你沒有說,電腦是不會自己執行你沒叫他做的事情的。因此,在這段程式碼當中,由於 time_remaining 在迴圈主體中永遠都不會改變,中止條件 time_remaining <= 0 也就永遠都不會成立,程式才會停不下來。我們必須明確描述「讓剩餘秒數減少」這個動作,才有可能可以讓 time_remaining > 0 的條件從成立變成不成立:

>>> time_remaining = 10
>>> while time_remaining > 0:
...     print(time_remaining)
...     time_remaining -= 1
... 
10
9
8
7
6
5
4
3
2
1
>>> 

基於這樣的原則,所有想要停下來的 while 迴圈主體都應該要含有可以讓他的運作條件從成立變成不成立的運算。如果運作條件是某數字大於某常數,那中止條件就是要讓該數字變成小於等於某常數,所以在迴圈主體當中就要想辦法讓該數字變小;如果運作條件是某數字不等於另一個數字,那中止條件就是要讓該數字等於另一個數字,在迴圈主體當中就要想辦法讓該數字最後會等於另一個數字;若運作條件是 A 條件和 B 條件同時都要成立,那中止條件就是 A 條件不成立或是 B 條件不成立,在迴圈主體當中就要想辦法讓 A 條件或 B 條件任一個(或兩個都)變得不成立以此類推。

4.1.2 無窮迴圈

如果迴圈主體沒有寫好,或是你本來就打算讓運作條件永遠都會成立,那麼 while 迴圈就會變成無窮迴圈,永遠無法執行到迴圈的後面。迴圈主體沒寫好的例子我們剛剛已經看過了,那本來就打算寫出無窮迴圈的話,要怎麼寫呢?很簡單,就讓運作條件是一個恆成立的條件表達式就可以了,最簡單的恆成立條件就是 True

>>> while True:
...     print("Y")
... 
Y
Y
Y
Y
Y
Y
Y
Y
Y

早期有一些程式語言並沒有內建布林型別,因此也有一些人的習慣是寫永遠都會成立的條件運算,例如:

>>> while 1 == 1:
...     print("Y")
... 
Y
Y
Y
Y
Y
Y
Y
Y
Y

由於不用思考怎麼從迴圈當中跳出來,無窮迴圈很容易就可以寫出來。但是寫得出來是一回事,怎麼用又是另一回事了。怎麼會有人需要一個永遠都跳不出來的迴圈呢?

事實上,無窮迴圈比想像的還要更常見。例如:許多的 RPG 遊戲都沒有「玩完」的一天,只要你存檔還在,每一次打開遊戲就會從上次的存檔處繼續執行下去。從邏輯上來說,遊戲中的世界是永遠都停不下的,因此只要遊戲軟體一開始執行、存檔被載入,遊戲的程式就會進入一個無窮迴圈,不斷重複遊戲當中的每一天。又例如:我們一天 24 小時都可以使用 IG 或 Line 的訊息服務,無論你在什麼時候傳訊息,訊息(通常)都可以被傳出去。這是因為 IG 或 Line 的伺服器被寫成了一個無窮迴圈,只要電腦沒關機,這支伺服器的程式永遠都要一直監聽有沒有人傳訊息,如果有的話就要把訊息送到對應的地方去。在現今商務電子化的世界中,有許多的服務是必須 24/7 全天候營運的,只要沒有停電,這些電腦程式就必須永無止盡的持續工作。這類應用程式的商業邏輯,往往就是被包在一個無窮迴圈中運作的。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Python 有 do-while 語句嗎?

如果你曾接觸過長得像 C 語言的程式語言(如: C, C++, Java, JavaScript 等),那你可能會看過 do-while 語句,他的語法如下

do {
    statements;
} while (conditional_expression);

do-while 語句用來表達「先做某動作,再檢查某條件,若該條件還成立,持續執行該動作」的語意。他跟 while 迴圈最大的差別是:不管一開始條件成不成立,某動作無論如何都會被執行一次。

但是,在 Python 當中並不存在 do-while 語句。根據 Stackoverflow 上的討論 , Python 不設計這種語句的原因是因為「找不到一個好方法,可以設計出符合 Python 當中定義複合語句的 statement: indented block 這個長相的 do-while 迴圈語法」。此外,也因為 do-while 語句可以直接用 while 語句模擬,因此其實也沒有額外再實作 do-while 語句的必要了。

4.2 for 迴圈

for 迴圈,或是 for 語句,用來表達「對於某範圍內的每一筆資料,執行一樣的動作」的語意。for 語句的語法如下:

>>> for item in iterable:
...     statement

這個語句的組成內容有:

  • 關鍵字 for
  • 迭代變數 item
  • 關鍵字 in
  • 可迭代物件 iterable
  • 冒號 :
  • 縮排
  • 一到多句的運算動作 statement

從上方的語法結構,你可以觀察到: Python 的 for 迴圈是沒有條件判斷式的,因為 for 迴圈並不依賴於條件判斷來決定自己什麼時候要停下來。 for 迴圈的行為非常容易理解,就是在每一輪的迴圈當中,把 iterable 當中的下一個東西拿出來(這東西就是 item),執行 statements,直到 iterable 中沒有任何東西可以拿了就停止。一般來說,如果我們會用到 for 迴圈,就代表我們是想要利用或是針對 iterable 裡面的物件進行一些操作,因此在 for 迴圈主體內的 statements 當中,通常會用到從 iterable 內拿出來的 item 變數(因為如果不會用到,那就沒有用 for 迴圈的必要了)。

Python 的 for 迴圈跟其他長得像 C 的語言的 for 迴圈非常不一樣。在那些語言當中, for 迴圈通常會由初始化語句、中止條件判斷、更新語句,與迴圈主體語句四個部份組成,而且這種 for 迴圈的行為是可以用 while 迴圈等價地實作出來的。但 Python 當中的 for 迴圈更像是其他語言當中的 for-each 迴圈,只有遍歷過一個可迭代物件的功能而已,沒辦法透過條件判斷來進行流程控制,也沒有辦法用 while 迴圈等價地實作出來。

4.2.1 可迭代物件 (Iterable)

講了這麼多,我們一直沒有解釋可迭代物件 (iterable) 這個新名詞到底是什麼意思。從名字聽起來,這東西好像很高深莫測,但其實說白了,就是「一個裏面裝著很多個東西的物件,而這裏面的東西可以一個一個拿出來」這樣而已。舉例來說, Python 內建的字串資料型別就是一個可迭代物件,因為一個字串裏面有很多個字元,而這些字元可以依照順序一個一個拿出來:

>>> s = "abcdefg"
>>> for c in s:
...     print(c)
... 
a
b
c
d
e
f
g
>>> 

除了字串之外,Python 內建的許多型別都是像這樣可以從裏面一個一個把東西拿出來的,像是列表 (list) 、數組 (tuple) 、集合 (set) 、字典 (dictionary) 等。在使用 for 迴圈遍歷這些資料型別時他們會有什麼樣的行為,我們都會在往後的課程當中提到。但有一種可迭代物件是我們第一次學習 Python 的 for 迴圈時一定要提到的,那就是 range 型別

4.2.2 range 型別

在學習 for 迴圈時,最經典的一個使用情境就是要用來數數 (count)。例如:要從 1 數到 5,我們可以先列舉出 1 到 5 之間的所有數字,放進一個列表裏面,然後用 for 迴圈一個一個印出來:

>>> for x in [1, 2, 3, 4, 5]:
...     print(x)
... 
1
2
3
4
5
>>> 

但如果今天你要數的不是 1 到 5,而是 1 到 100 呢?這下該不會要自己列舉從 1 到 100 之間的 100 個數字吧?那如果要印的是 1 到 1000 呢? 1 到 10000 呢?光是列舉數字整個銀幕的畫面就被佔滿了,我們當然不會用這麼沒效率的方式來寫程式。但數數又是一個很常做的行為,因此 Python 專門設計了一種資料型別,來幫我們做數數這件事,而這個資料型別就是 range 型別

"range" 就是「範圍」的意思。 range 型別顧名思義,就是一個可以自動產生某範圍內數字的物件。透過呼叫 range 型別的建構函式 range() ,然後傳入參數,它就會回傳一個可以列舉該範圍內數字的可迭代物件。

>>> a = range(5)
>>> for x in a:
...     print(x)
... 
0
1
2
3
4
>>> 

range() 建構函式的定義為 range(start, stop, step),一共有三個參數可以傳入。這三個參數的功能分別為

  • start: 範圍的開頭,在數數的時候會從這個數字開始數。不一定要傳入,如果沒有傳入,預設是 0
  • stop: 範圍的結尾,但不包含這個數字,也就是說數數的時候只會數到這個數字的前一個人。一定要傳入
  • step : 每隔幾個數字一數。不一定要傳入,如果沒有傳入,預設是 1

當我們呼叫 range(start, stop, step) 時,會得到一個從 start 開始數,每 step 一數,數到 stop 之前為止(也就是不包含 stop )的可迭代物件。其中,由於 startstep 已經有預設值了,所以在呼叫 range() 時,最少可以只要傳入一個數字就好,最多則可以傳入三個數字。

如果只有傳入一個數字,那麼該數字就會被填入 stop 的位置。以上方程式碼範例來說,我只有呼叫 range(5),那就相當於是呼叫了 range(0, 5, 1) ,會得到一個從 0 開始數,每 1 一數,數到 5 之前為止的可迭代物件。當我們用 for 迴圈遍歷過這個物件時,就可以把那裡頭的每一個數字都拿出來,會得到 0, 1, 2, 3, 4。

如果傳入兩個數字,那麼第一個數字會被填入 start 的位置,第二個數字則會被填入 stop 的位置。舉例來說,當我們呼叫 range(1, 10) ,那就相當於是呼叫了 range(1, 10, 1),會得到一個從 1 開始數,每 1 一數,數到 10 之前為止的可迭代物件。當我們用 for 迴圈遍歷過這個物件時,會得到 0, 1, 2, , 9。

>>> for x in range(1, 10):
...     print(x)
... 
1
2
3
4
5
6
7
8
9
>>> 

如果傳入三個數字,那麼這三個數字就會依序被填入 start, stop, step 這三個位置。舉例來說,當我們呼叫 range(5, 50, 5) ,會得到一個從 5 開始數,每 5 一數,數到 50 之前為止的可迭代物件。當我們用 for 迴圈遍歷過這個物件時,會得到 5, 10, 15, , 40, 45

>>> for x in range(5, 50, 5):
...     print(x)
... 
5
10
15
20
25
30
35
40
45
>>> 

如果 step 被傳入一個負值,並且 stopstart 還要小的話,那這個 range 就會倒著數:

>>> for x in range(5, 0, -1):
...     print(x)
... 
5
4
3
2
1
>>> 

使用 range 物件,基本上就可以應付各種需要數數的使用情境了。舉凡重複執行同樣的動作數次、對某範圍內的數字進行運算、列舉出一個陣列內的每個東西等等,都會使用到 range 函數。以下的程式範例,就是一個「產生 n 個隨機亂數」的程式:

# random.py
import random

n = int(input("Give an integer n: "))
for i in range(n):
    print(random.random())
$ python3 random.py 
Give an integer n: 10
0.26023750037439575
0.4850533662093761
0.05602333731779807
0.039464476625664546
0.7370281332142026
0.07695208445268087
0.5279437400197055
0.8346885854615177
0.37037268050964145
0.4972495510433438
$ 

在這個程式中,我們首先要求使用者指定要產生多少個亂數,然後利用 for 迴圈和 range 物件重複執行使用者指定次數的「產生並印出一個亂數」這個動作。

關於更多 range 型別的使用方法,我們會藉由練習題來多加認識。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
為什麼 range() 預設是從 0 開始數,又不數到 stop 呢?

這是因為 range 很常被拿來用在列舉一個陣列的索引值 (index)。 在 Python (以及絕大部分的程式語言)中,陣列的索引是從 0 開始,直到陣列長度 -1 的。也就是說,如果我有一個長度為 3 的陣列(裏面裝有三個東西的陣列),那麼第一個東西的索引值會是 0 ,第二個會是 1 ,第三個會是 2 。 range() 建構函式的預設值之所以會被那樣設計,就是為了讓我們可以用最少的程式碼完成這個非常常進行的動作。

4.3 進階迴圈流程控制

現在我們已經知道如何利用迴圈重複地執行同一個任務了。但是在某些情境下,我們會希望在迴圈執行的過程中改變它的執行流程。在這種時候,我們就會需要用到 break 語句或是 continue 語句。

4.3.1 break 語句

當我們希望在迴圈執行到一半時,直接離開這個迴圈,繼續執行迴圈後的指令,就可以使用 break 語句

# break.py a = 5 print("Before while loop, a =", a) while a > 0: a -= 1 print("Before break, a =", a) if a == 3: break print("After break, a =", a) print("After while loop, a =", a)
$ python3 break.py
Before while loop, a = 5
Before break, a = 4
After break, a = 4
Before break, a = 3
After while loop, a = 3
$

在迴圈當中,若執行到 break 語句,程式會直接從 break 那一行跳到迴圈結構的下一行,也就是直接跳出迴圈的意思。以上方程式範例來說,程式第一次跳進迴圈時, a 被 -1 之後, a = 4 ,由於並不滿足第 8 行的 if 語句執行條件,因此不會執行到 break 語句,整個迴圈主體都會被完整執行;但在下一次再次進入迴圈, a 又被 -1 後, a = 3 ,由於 if 語句的執行條件被滿足了,所以就會執行到第 9 行的 break 語句。一旦執行到 break 語句,不管當下迴圈的執行條件是否還有滿足,不管 break 後面還有沒有指令,程式的下一個執行步驟都會直接跳到迴圈結尾的下一行,也就是跳到第 11 行去。這就是為什麼程式的輸出結果,在輸出了 "Before break, a = 3" 之後,既沒有輸出 "After break, a = 3" (沒有執行迴圈剩下的部份),也沒有輸出 a = 2 之後的執行結果(沒有把迴圈剩下的回合跑完),就直接輸出了 "After while loop, a = 3" 。你可以觀察到跳出迴圈的時候, while 迴圈的執行條件其實還是滿足的( a = 3 依然滿足 a > 0 ),可見 break 是會無條件地直接跳出迴圈的。

break 語句使用在 for 迴圈上也會得到相同的效果:

# break_for.py

for x in range(5):
    print("Before break, x =", x)
    if x == 2:
        break
    print("After break, x =", x)
print("After for loop")
$ python3 break_for.py
Before break, x = 0
After break, x = 0
Before break, x = 1
After break, x = 1
Before break, x = 2
After for loop
$

break 語句結合我們先前提到的無窮迴圈,就會形成一個很有彈性的流程控制工具。在某些情境下,我們很難一語道盡迴圈的停止條件,尤其是我們有大量的程式邏輯被放在同一個超大的迴圈裏面時。這些不同的程式邏輯可能各自維護著各種狀態變數,而我們要不要跳出這個迴圈的依據可能會隨著這些狀態的各種排列組合而定。比起在迴圈的開頭寫滿複雜又難懂的中止條件,不如直接寫一個無窮迴圈,然後把是否要跳出迴圈的控制邏輯寫在迴圈的內部,有時會反而還更好懂一些。

4.3.2 continue 語句

當我們希望在迴圈執行到一半時,跳過這一輪剩下的步驟,直接進入迴圈的下一輪,就可以使用 continue 語句

# continue.py a = 5 print("Before while loop, a =", a) while a > 0: a -= 1 print("Before continue, a =", a) if a == 3: continue print("After continue, a =", a) print("After while loop, a =", a)
$ python3 continue.py
Before while loop, a = 5
Before continue, a = 4
After continue, a = 4
Before continue, a = 3
Before continue, a = 2
After continue, a = 2
Before continue, a = 1
After continue, a = 1
Before continue, a = 0
After continue, a = 0
After while loop, a = 0
$

continue.py 當中的程式碼跟 break.py 當中的程式碼幾乎是一模一樣的,差別只在第 9 行從 break 語句變成了 continue 語句。在迴圈當中,若執行到 continue 語句,程式會從 continue 的那一行跳回迴圈的開頭,進入下一輪的迴圈。具體來說,如果是在 while 迴圈裡頭遇到 continue ,那麼程式會跳過 continue 之後的所有程式碼,直接回到 while 那一行重新進行條件判斷,再決定要不要繼續執行迴圈;如果是在 for 迴圈裡頭遇到 continue ,那麼程式會跳過 continue 之後的所有程式碼,直接回到 for 那一行,取出下一個物件,然後繼續執行迴圈。

觀察上方程式碼的輸出結果,這次我們會發現,迴圈有好好的執行到迴圈中止條件成立之後才跳出。但是仔細觀察 a = 3 那一次的輸出,會發現只有 "Before continue, a = 3" 被輸出出來, "After continue, a = 3" 這一條被跳過了沒有輸出,反而是直接印出了下一輪的 "Before continue, a = 2" 。這代表程式在執行到第 9 行的 continue 語句之後,跳過了 continue 之後到迴圈結束為止的所有程式碼,直接回到了第 5 行的 while 迴圈開頭,再進行下一輪的迴圈。

為什麼我們知道是回到第 5 行,而不是第 6 行(迴圈主體的第一行)呢?我們可以來看看下面這個程式範例:

# continue.py a = 5 print("Before while loop, a =", a) while a > 0: a -= 1 print("Before continue, a =", a) if a == 3: a = 0 continue print("After continue, a =", a) print("After while loop, a =", a)
$ python3 continue.py
Before while loop, a = 5
Before continue, a = 4
After continue, a = 4
Before continue, a = 3
After while loop, a = 0
$

上方的程式碼是在原本的 continue.py 裡多加了第 9 行的 a = 0 。結果我們發現,在程式輸出 "Before continue, a = 3" 之後,就直接輸出了 "After while loop, a = 0" ,而不是再執行一次迴圈的主體。從這樣的結果我們就可以推測, continue 語句執行之後,程式的執行流程是回到了第 5 行去重新判斷了迴圈的中止條件,而不是回到第 6 行無條件執行下一次的迴圈。如果 continue 是回到迴圈主體內的第一行的話,那麼上面這段程式碼照裡來說應該還要多輸出 "Before continue, a = -1" 和 "After continue, a = -1" 這兩行,才會輸出 "After while loop, a = 0" 才對。

continue 語句使用在 for 迴圈也會有相同的效果:

# continue_for.py

for x in range(5):
    print("Before continue, x =", x)
    if x == 2:
        continue
    print("After continue, x =", x)
print("After for loop")
$ python3 continue_for.py
Before continue, x = 0
After continue, x = 0
Before continue, x = 1
After continue, x = 1
Before continue, x = 2
Before continue, x = 3
After continue, x = 3
Before continue, x = 4
After continue, x = 4
After for loop
$

使用 continue 語句,我們可以在自動化的過程當中跳過一些不符合特定條件的資料不處理,為迴圈的行為增加了更多彈性。

4.3.3 while-else 語句與 for-else 語句

想像一個問題情境:我們想要在一個序列裏面尋找某一筆資料,如果有找到的話,我們想要知道關於這筆資料的更多細節,但如果沒找到的話,我們也想要印出一個「找不到資料」的提示文字。要完成這個任務,我們可以怎麼做呢?

以「在使用者給定的區間內找尋第一個 13 的倍數」這個問題為例好了,第一個瞬間你想到的寫法可能會是像這樣的:

# find_multuple_of_13.py

start = int(input("輸入區間的開頭: "))
stop = int(input("輸入區間的結尾: "))

found = False
for n in range(start, stop):
    if n % 13 == 0:
        print(n, "是 13 的倍數")
        found = True
        break

if not found:
    print("沒有找到 13 的倍數")
$ python3 find_multuple_of_13.py
輸入區間的開頭: 290
輸入區間的結尾: 314
299 是 13 的倍數
$ python3 find_multuple_of_13.py
輸入區間的開頭: 889
輸入區間的結尾: 896
沒有找到 13 的倍數
$ 

我們可以使用一個 found 的旗標(見下方補充說明區塊),在迴圈運行的過程中紀錄目前是否已經找到了資料。旗標一開始要設置成 False ,表示尚未找到資料。如果迴圈運行過程中有找到資料,就將 found 設置為 True ,並跳出迴圈;如果整個迴圈都跑完了還是沒有找到資料,那麼 found 就會始終維持是 False 的狀態。而我們只需要在迴圈結束後檢查 found 旗標的值,就可以知道剛剛的迴圈過程中有沒有找到資料了,如果沒找到再印出提示訊息。

在其他語言當中,你可能只有上面這一種寫法。但是在 Python 當中,有一種專門為了這種情境被設計出來的語法,就是 while-else 語句跟 for-else 語句。就像 if-else 語句一樣, Python 允許你在迴圈的尾巴後面加一個 else 語句,該語句內的指令只有在迴圈的運作條件為假之後才會執行到。以 while-else 語句來說,那就是在 while 語句中的 conditional_expression 變成 False 之後,才會執行到 else 語句裡的東西;如果是 for-else 語句,那就是 for 語句中的 iterable 已經再也拿不出東西來了,才會執行到 else 語句裡的東西。

>>> while conditional_expression:
...     do_something()
... else:
...     execute_after_while_loop_end()
>>> for item in iterable:
...     do_something()
... else:
...     execute_after_for_loop_end()

我們在 4.3.1 當中有提到,如果使用 break 語句,由於 break 是會無條件的跳出迴圈的,所以在使用 break 離開迴圈後,迴圈的執行條件有時候還會是維持成立的狀態的。因此,如果使用 break 語句搭配迴圈的 else 語句的話,就可以很輕鬆的處理我們上方提到的這個找東西的問題:

# find_multuple_of_13.py

start = int(input("輸入區間的開頭: "))
stop = int(input("輸入區間的結尾: "))

for n in range(start, stop):
    if n % 13 == 0:
        print(n, "是 13 的倍數")
        break
else:
    print("沒有找到 13 的倍數")
$ python3 find_multuple_of_13.py
輸入區間的開頭: 290
輸入區間的結尾: 314
299 是 13 的倍數
$ python3 find_multuple_of_13.py
輸入區間的開頭: 889
輸入區間的結尾: 896
沒有找到 13 的倍數
$ 

注意上方程式碼的縮排: else 的縮排是跟 for 在同一層的,而不是跟 for 迴圈裡頭的 if 同一層。這表示 else 跟隨的是 for 的迴圈結構,只有在 for 迴圈的運作條件不成立(把整個 range 都掃完了)時才會執行,而不是在 n % 13 != 0 時執行。程式碼的輸出結果跟原本使用 found 旗標的結果相同,但是卻少了維護旗標的動作,讓程式碼寫起來更加精簡了。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
什麼是旗標

旗標 (flag) 是程式設計的一個專有名詞,泛指用來維護某個狀態的布林值。有時我們會希望用一個變數來紀錄某個條件是成立還是不成立(例如:開關現在是開還是關?在尋找的資料是已經找到還是還沒找到?系統資源現在是有人在使用還是沒有人在使用?),這個條件會隨著時間點的不同,有時候是成立,有時候是不成立,而根據此條件成立與否,我們的程式需要做出不同的反應。在這種時候使用到的布林變數,就被稱作旗標。

4.4 巢狀迴圈

我們已經學會了迴圈結構的運作規則,現在讓我們回憶一下本章節最開始時所提到的「普通上班族的生活模式」:

while age < retirement_age:
    for day in ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']:
        if day == 'Saturday' or day == 'Sunday':
            take_a_day_off()
        else:
            work()
    # age increases every 365 days

在這個程式碼範例當中,有一個 for 迴圈被宣告在一個 while 迴圈當中,我們可以觀察到一個「在迴圈中執行迴圈」的結構。就如同我們在 3.3 節當中提到的,一個程式碼區塊當中可以包含更多的程式碼區塊,而我們稱呼這樣的結構叫作巢狀的程式碼區塊 (nested block)。迴圈語句的主體同樣也是由程式碼區塊組成的,因此我們當然可以在迴圈本體內部宣告其他的程式碼區塊,包含 if 語句跟更多的迴圈。當一個迴圈當中包含更多層的迴圈時,我們就稱呼這樣的結構叫作巢狀迴圈 (nested loop)

有時候,我們會需要重複執行「一件需要重複執行的事」,為了表達這樣的意思,我們就需要建構一個巢狀迴圈。像是在如上的程式碼中,我們以 for 迴圈來對「一個禮拜中的每一天」進行重複的動作;而「一個禮拜的每一天」這個重複的動作,需要在我們到達退休年紀之前不斷的被重複執行,因此我們必須把這個 for 迴圈包在一個 while 迴圈當中。當然,無論是在 while 迴圈還是 for 迴圈,裏面都是可以任意放置更多的 while 迴圈或 for 迴圈的,一切都是取決於我們想要解決什麼樣的問題。

第一次學習巢狀迴圈時,我們有時會搞不清楚程式的執行順序。比起直接說明原理,我們先用以下這個簡單的程式碼範例來練習分析,可能會讓你更了解如何拆解巢狀結構執行的順序:

>>> for i in range(3): ... for j in range(3): ... print('i =', i, ', j =', j) ... i = 0 , j = 0 i = 0 , j = 1 i = 0 , j = 2 i = 1 , j = 0 i = 1 , j = 1 i = 1 , j = 2 i = 2 , j = 0 i = 2 , j = 1 i = 2 , j = 2 >>>

首先我們觀察到,上方程式總共輸出了九行文字,而程式碼中只有第 3 行是 print() 函式呼叫,因此我們可以確定第 3 行一共被執行了九次。再來,我們看到這九行輸出可以大致被分成 3 組: i = 0 時一組、 i = 1 時一組,跟 i = 2 時一組。在每一組 3 行的輸出當中, 變數 j 都經歷了 j = 0, j = 1j = 2 的三個階段。也就是說,變數 i 改變一次,變數 j 就會改變三次,因此我們可以得知:每執行一輪第 1 行的 for 迴圈,就必須要完整執行一次(三輪)第 2 行的 for 迴圈

要解釋這樣的輸出結果,我們可以根據迴圈的執行邏輯,手動展開 (unroll) 迴圈。在展開迴圈時,沒有人規定要從外面往裡面拆還是從裡面往外面拆。但由於愈內層的迴圈(或程式碼區塊)往往包裹著愈單純的邏輯(愈少的程式碼),因此為了分析和觀察上的方便,我們通常會從裡層往外層拆解

for 迴圈的執行邏輯,是「對於 iterable 中的每一個物件,重複執行一樣的動作」。而以上方的程式碼的第 2 行(內層迴圈)來說,這裡的 iterable 就是 range(3) ,我們會在迴圈重複執行的過程當中,分別從裡頭拿出 0, 1, 2 三個值出來,並執行迴圈本體的 print() 動作。因此,我們第一步可以先將第 2 行的 for 迴圈拆解如下:

>>> for i in range(3): ... j = 0 ... print('i =', i, ', j =', j) ... j = 1 ... print('i =', i, ', j =', j) ... j = 2 ... print('i =', i, ', j =', j) ...

拆解至此,我們就不難發現為何在變數 i 等於一個固定的數字時,變數 j 會有三次的變化了。因為將內層 for 迴圈展開的結果,正是分別將 j 指派了 0, 1, 2 三個值,然後再印出來。這一大串程式碼就是外層 for 迴圈的主體,也就是它執行一輪會做的所有事情。因此,如果我們還要繼續展開外層的 for 迴圈,那我們就必須把這一大串程式碼完整複製三次(因為外層也是 range(3)),變成:

>>> i = 0 >>> j = 0 >>> print('i =', i, ', j =', j) # i = 0, j = 0 >>> j = 1 >>> print('i =', i, ', j =', j) # i = 0, j = 1 >>> j = 2 >>> print('i =', i, ', j =', j) # i = 0, j = 2 >>> i = 1 >>> j = 0 >>> print('i =', i, ', j =', j) # i = 1, j = 0 >>> j = 1 >>> print('i =', i, ', j =', j) # i = 1, j = 1 >>> j = 2 >>> print('i =', i, ', j =', j) # i = 1, j = 2 >>> i = 2 >>> j = 0 >>> print('i =', i, ', j =', j) # i = 2, j = 0 >>> j = 1 >>> print('i =', i, ', j =', j) # i = 2, j = 1 >>> j = 2 >>> print('i =', i, ', j =', j) # i = 2, j = 2 >>>

如此一來,就展開出了九次的 print() 函式呼叫,從上到下分別就對應到了原本程式碼的九行輸出。你也可以試試先拆解外層的迴圈,再拆解裡層的迴圈,看看是否會得到一樣的結果。

利用這樣的分析方式,我們就有辦法分析結構更複雜的迴圈了。無論是兩層以上的迴圈、迴圈本體混有迴圈和其他語句的迴圈,還是 while 迴圈和 for 迴圈層層套疊的迴圈,只要掌握上述這種展開程式碼的原則,我們都能夠輕易掌握整個程式的執行流程。

最後,我們來看看下面這段程式碼:

>>> for i in range(3): ... for j in range(3): ... print('i =', i, ', j =', j) ... break ...

這一段程式碼會輸出什麼呢?是只會輸出一行 i = 0, j = 0 就停止嗎?要回答這個問題,我們就必須釐清 break 語句(和 continue 語句)作用在巢狀迴圈時,可以跳出的範圍如何界定。

break 語句和 continue 語句,當作用在巢狀的迴圈結構當中時,只能跳出/跳過離自己最近的那一個迴圈而已。以上方的程式碼範例來說,第 4 行的 break 離第 2 行的 for 迴圈最近,所以當執行到 break 時,它只會跳出第 2 行的 for 迴圈,不會跳出第 1 行的 for 迴圈。因此,這段程式碼的輸出結果是這樣的:

>>> for i in range(3): ... for j in range(3): ... print('i =', i, ', j =', j) ... break ... i = 0 , j = 0 i = 1 , j = 0 i = 2 , j = 0 >>>

我們可以觀察到,變數 j 每次都只有被輸出 j = 0 一行,但是這一行隨著變數 i 的變化被輸出了三次。這表示內層的 for 迴圈每次都只有執行一輪,就被無條件 break 出來了,但是外層的 for 迴圈並不受到 break 的影響,完整地執行了三輪。同樣的道理,如果是使用 continue 語句來跳過迴圈後面不想執行的程式碼,也會有類似的效果。只有在和 continue 同一層的程式碼區塊會被跳過,在 continue 上一層的迴圈並不會受到影響。

現在,我們已經了解了如何重複執行同樣的指令,我們的程式碼已經可以自動化地幫我們處理許多枯燥乏味的工作了。但就如同我們在 3.4 節中提到的,重複的動作搭配上有規律的資料,才是我們在處理現實世界中的問題時,最常需要進行自動化的工作。因此,在下一章當中,我們將要認識到 Python 當中最基礎,也是最常被使用到的資料結構 —— 列表。

Excersices

4.1 金字塔

給定一個奇數 n ,印出一個高度為 n 層的金字塔形狀。一個合理的金字塔形狀應該要符合以下條件:

  • 第一層有一個 ,接著每一層都比上一層多兩個
  • 要使用全形空白   進行合理的縮排,使得金字塔形狀左右對稱

如果使用者輸入了不是奇數的數字,重複要求使用者輸入直到得到奇數為止。

$ python3 pyramid.py 
請輸入金字塔的層數(只能是奇數): 1

金
$ python3 pyramid.py 
請輸入金字塔的層數(只能是奇數): 3

  金
 金金金
金金金金金
$ python3 pyramid.py 
請輸入金字塔的層數(只能是奇數): 4
請輸入金字塔的層數(只能是奇數): 5

    金
   金金金
  金金金金金
 金金金金金金金
金金金金金金金金金
$ 

(大家快去聽我女神唱歌!!!

提示: 使用 print("text", end="") 可以使得 print() 印完文字後不換行。

4.2 Fizz Buzz

給定一個整數 n,輸出 1n 之間的整數,但

  • 若當前數字是 3 的倍數,則改為輸出 Fizz
  • 若當前數字是 5 的倍數,則改為輸出 Buzz
  • 若當前數字同時是 3 和 5 的倍數,則改為輸出 FizzBuzz
$ python3 FizzBuzz.py 
請輸入一個整數: 20
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
$ 

4.3 找質數

給定一個大於 1 的整數 n ,如果 n 是一個合數,則印出他的第一組因數分解,否則印出 [n] 是一個質數

$ python3 prime.py 
請輸入一個大於 1 的整數: 2
2 是一個質數
$ python3 prime.py 
請輸入一個大於 1 的整數: 6
6 = 2 * 3
$ python3 prime.py 
請輸入一個大於 1 的整數: 49
49 = 7 * 7
$ python3 prime.py 
請輸入一個大於 1 的整數: 91
91 = 7 * 13
$ python3 prime.py 
請輸入一個大於 1 的整數: 101
101 是一個質數

4.4 九九乘法表

印出一個格式如下的九九乘法表。

(有點志氣,不要用寫死的!)

$ python3 9x9.py 
2 * 1 = 2	3 * 1 = 3	4 * 1 = 4	5 * 1 = 5	
2 * 2 = 4	3 * 2 = 6	4 * 2 = 8	5 * 2 = 10	
2 * 3 = 6	3 * 3 = 9	4 * 3 = 12	5 * 3 = 15	
2 * 4 = 8	3 * 4 = 12	4 * 4 = 16	5 * 4 = 20	
2 * 5 = 10	3 * 5 = 15	4 * 5 = 20	5 * 5 = 25	
2 * 6 = 12	3 * 6 = 18	4 * 6 = 24	5 * 6 = 30	
2 * 7 = 14	3 * 7 = 21	4 * 7 = 28	5 * 7 = 35	
2 * 8 = 16	3 * 8 = 24	4 * 8 = 32	5 * 8 = 40	
2 * 9 = 18	3 * 9 = 27	4 * 9 = 36	5 * 9 = 45	

6 * 1 = 6	7 * 1 = 7	8 * 1 = 8	9 * 1 = 9	
6 * 2 = 12	7 * 2 = 14	8 * 2 = 16	9 * 2 = 18	
6 * 3 = 18	7 * 3 = 21	8 * 3 = 24	9 * 3 = 27	
6 * 4 = 24	7 * 4 = 28	8 * 4 = 32	9 * 4 = 36	
6 * 5 = 30	7 * 5 = 35	8 * 5 = 40	9 * 5 = 45	
6 * 6 = 36	7 * 6 = 42	8 * 6 = 48	9 * 6 = 54	
6 * 7 = 42	7 * 7 = 49	8 * 7 = 56	9 * 7 = 63	
6 * 8 = 48	7 * 8 = 56	8 * 8 = 64	9 * 8 = 72	
6 * 9 = 54	7 * 9 = 63	8 * 9 = 72	9 * 9 = 81

提示: 使用 print('...', end='\t') 進行適當的排版。


<< Lec 03 - 流程控制 | 目錄 | Lec 05 - 陣列與數組 >>