跳至主要内容

迴圈

迴圈

小時候上學的時候有沒有做錯事被罰寫的經驗?我小時候很頑皮,所以常被老師處罰。老師可能會要我罰寫 100 遍的「我以後不會亂丟垃圾」,一般人只能一遍一遍的寫,厲害一點的可以單手拿五隻筆就可以一口氣寫五遍,但是如果你會使用迴圈(Loop),你只需要寫一次程式,就可以讓叫電腦幫我們寫,而且想寫幾次都行。

重複的事交給迴圈做

現在大家應該知道怎麼在畫面上印東西出來了,如果我要你在畫面上印出 1 到 10 的數字,你會怎麼做?可能你會這樣寫:

print(1)
print(2)
print(3)
# ... 略 ...
print(10)

雖然感覺有點麻煩但沒什麼問題。數字小還好,如果要你印到 100、1000 或是更大的數字呢?這時候就可以交給迴圈來處理了。在 Python 的迴圈有兩種,一種是 for 迴圈,另一種是 while 迴圈。

for 迴圈

for 迴圈的格式是:

for 東西 in 一堆東西:
做一些事

這裡的 forin 關鍵字是固定的寫法,而「一堆東西」可以是一個字串、一個串列或是一個範圍(Range)等等可以進行「迭代(Iteration)」操作的物件。在迴圈轉轉轉的過程中,每回合會從這一堆東西裡依序一個一個把裡面的東西,又稱之「元素(Element)」拿出來。

Python 是使用縮排做為程式碼區塊的標示,在 for 迴圈裡面的程式碼也一樣都要縮排一下(不縮排程式會無法執行),這樣 Python 才知道這些程式碼是屬於這個迴圈的。如果 for 迴圈裡的程式碼區塊裡面暫時還不知道要做什麼事,可以使用 pass 關鍵字卡個位:

for 東西 in 一堆東西:
pass # 不做事,就卡個位置

這樣語法才不會出錯。剛才說要在畫面上印出 1 到 10 的例子:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

for num in numbers:
print(num)

中括號包起來的一堆東西稱之「串列(List)」,在下個章節就會介紹到,你可以把它看成一個裝著很多東西的盒子,裡面分別裝了 1 到 10 的數字。for 迴圈裡的變數 num 你可以自己決定用什麼名字,其他的變數命名規範一樣,避開關鍵字就好。

不過這樣還是得自己手刻一個串列出來,數字一多也是有點麻煩,Python 有一個內建型別 range ,可以幫我們快速產生一個數字「範圍」:

for num in range(1, 11):
print(num)

range() 可以幫我們造出一個範圍,這個函數的用法跟我們之前在字串介紹的切片(Slice)有點像,它的三個參數分別是「起始位置(Start)」、「停止位置(Stop)以及「移動距離(Step)」,所以這裡寫著 range(1, 11) 的意思是從 1 開始,到 11 但不包括 11,每次預設移動 1。順便再提一下,在 Python 2 的時候 range() 函數會直接產生一個串列:

# Python 2
>>> range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

但為了效能考量,Python 3 的 range 改成產生一個範圍物件,物件什麼的我們等到後面講到物件導向章節再詳細說明,你暫時先把物件看成一顆石頭或冬瓜或其他什麼東西就好。改成這樣的好處是不用一口氣把所有數字都產生出來,只有在需要的時候才會產生,節省記憶體空間。不過如果你想要像 Python 2 一樣得到一個串列的話,可使用內建的 list() 函數(更精準的說是 list 類別)來做轉換:

# Python 3
>>> range(10)
range(0, 10)

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

不只是串列或範圍能這樣做,只要能夠進行「迭代」的物件都可以這樣做,例如字串:

for char in "hellokitty":
print(char)

這樣就會從 h 開始一個字一個字的印出來,直到 y 為止。

不過有件需要特別注意的事情是,在 for 迴圈的過程中的變數,在迴圈結束之後依舊是會存在的,甚至在迴圈裡面宣告的變數,迴圈結束之後也都還會存在,我舉個比較極端一點的例子:

num = "hellokitty"

# ... 中間可能有很多程式碼 ...

for num in range(10):
hey = "hello"
print(num)

print(num) # 印出 9
print(hey) # 印出 hello

在上面的範例中,一開始我的 num 變數是 "hellokitty",但後來在 for 迴圈的過程中「不小心」也宣告了一個同名的 num 變數,這樣一來原本的 num 變數的值就被改掉了,所以在迴圈結束之後,原本的 num 變數就變成了 9,而且在迴圈裡面宣告的 hey 變數在迴圈外面也依然存在。

索引值

如果各位有接觸過其他的程式語言,有些程式語言的 for 迴圈裡面會有一個變數會不斷變化的索引值,我用 JavaScript 舉個例子:

const heroes = ["悟空", "達爾", "蜘蛛人", "蝙蝠俠"]

for (let i = 0; i < 4; i++) {
console.log(i + 1, heroes[i])
}

在迴圈的過程的變數 i 會一直變化,有時候可以用來當做編號或更常被拿來當做陣列的索引值,所以上面這樣程式執行之後會得到這樣的結果:

1 悟空
2 達爾
3 蜘蛛人
4 蝙蝠俠

在 Python 的 for...in... 寫法只能得到元素本身,沒有索引值可以用,你可能會想到這樣寫:

heroes = ["悟空", "達爾", "蜘蛛人", "蝙蝠俠"];

i = 0
for hero in heroes:
print(f"{i + 1} {hero}")
i += 1

在 Python 有更「Python 風味(Pythonic)」的寫法,就是使用內建型別 enumerate,它可以把串列轉換成帶有數字的元素組合:

>>> heroes = ["悟空", "達爾", "蜘蛛人", "蝙蝠俠"]
>>> list(enumerate(heroes))
[(0, '悟空'), (1, '達爾'), (2, '蜘蛛人'), (3, '蝙蝠俠')]

我這裡先用 list()enumerate 處理過的串列再轉換成串列,主要的目的是先給大家看看它現在是什麼樣子,可以看的出來經過 enumerate 處理過之後會變成數字跟元素的組合,更精準的說它把原本的串列轉換成一堆 Tuple 的串列,關於 Tuple 在第九章會有更完整的介紹,先把它當做另外一種裝資料的容器就行了。enumerate 的第二個參數可以指定起始的索引值,沒有指定就會從 0 開始:

# 指定從 1 開始
>>> list(enumerate(heroes, 1))
[(1, '悟空'), (2, '達爾'), (3, '蜘蛛人'), (4, '蝙蝠俠')]

# 指定從 100 開始
>>> list(enumerate(heroes, 100))
[(100, '悟空'), (101, '達爾'), (102, '蜘蛛人'), (103, '蝙蝠俠')]

看懂用法後,接著回到 for...in... 就可以拿到那個數字了:

heroes = ["悟空", "達爾", "蜘蛛人", "蝙蝠俠"]

for i, hero in enumerate(heroes, 1):
print(f"{i} {hero}")

因為在 in 後面的串列是一個充滿 Tuple 的串列,所以在 for 迴圈裡面可以用兩個變數來接它,這樣就可以得到跟其他程式語言一樣的結果了。

《練習》九九乘法表

我應該不需要解釋什麼是九九乘法表,很多的程式教學包括學校的教課書上只要介紹到迴圈,大概都有印出九九乘法表的練習。九九乘法表是由兩個數字相乘而來的結果,所以用迴圈應該可以輕鬆印出來

迴圈跟一個便當吃不夠可以吃兩個一樣,寫一層迴圈不夠的話可以寫兩層,想再多寫幾層也行,但這裡應該只要利用兩層迴圈的寫法就可以把九九乘法表給印出來:

# 九九乘法表

for i in range(1, 10):
for j in range(1, 10):
print(f"{i} x {j} = {i * j}")

外層的迴圈會先從 1 跑到 9,內層的迴圈也會從 1 跑到 9,所以這樣一來就可以把所有的九九乘法表都印出來了。

等大家程式再寫久一點,會發現在迴圈界有個神奇的慣例,不管哪個程式語言,就是在迴圈裡面的變數都會叫做 i。真正的典故已不可考,但我猜 i 可能是代表「索引(Index)」或「整數(Integer)」的意思。有趣的是,如果迴圈裡還有迴圈,內層的迴圈的變數名稱就會用 j,如果再裡面一層就是 k,依此類推,我猜這應該是工程師懶得想變數名稱的關係。所以,除非是單純的只用來當索引,不然我是不太建議用 ijk 這種比較沒有意義的變數名稱。

《練習》聖誕樹

聖誕樹也是很經典的迴圈練習,例如我想在畫面上印出一個 5 層的聖誕樹(?),有只有半邊的簡易版、左右對稱的標準版、有加上樹幹的進階版以及閃亮亮的豪華版,大概像這樣:

*              *             *               %
** *** *** % *
*** ***** ***** % * %
**** ******* ******* % * % *
***** ********* ********* % * % * %
* *
* *
簡易版 標準版 進階版 豪華版

先不管這東西到底長的像不像聖誕樹,我們就想辦法用程式把它們畫出來就好。在開始寫之前跟大家講一個小技巧,在 Python 裡如果把某個字元或字串乘上一個數字的話,就可以把這些字重複 N 次,例如:

print("*" * 10)      # 印出 **********
print("sorry " * 5) # 印出 sorry sorry sorry sorry sorry

先從簡易版開始:

# 簡易版

for n in range(5):
print("*" * (n + 1))

不用太複雜的計算也不需要置中對齊,一個簡單的迴圈就可以把這個簡易版的聖誕樹給印出來了。之所以裡面需要加 1 是因為 range() 做出來的範圍預設是從 0 開始的。接著是標準版,這個版本稍微複雜一點點,可能需要想一下,* 字元在第 1 層有 1 個,第 2 層有 3 個,第 3 層有 5 個...依此類推,所以每一層的數量計算公式應該是 2 x n + 1。接著,為了要讓出來的 * 有置中效果,所以在每一層的前面都要補上幾個空白字元來做對齊,第 1 層補 4 個空白、第 2 層補 3 個、第 3 層補 2 個...所以公式是 4 - n,稍微組合一下應該就能印出標準版的聖誕樹了:

# 標準版

for n in range(5):
print(" " * (4 - n) + "*" * (2 * n + 1))

除了乖乖的算要補幾個空白之外,在前面章節學到的 F 字串也可以拿出來用:

# 標準版(使用 F 字串)

for n in range(5):
print(f'{"*" * (2 * n + 1):^15}')

還記得 F 字串的對齊功能嗎?: 後面接的 ^15 表示寬度有 15 個字,而且是置中對齊,這樣我們就可以不用算前面要補幾個空白了。

最後是進階版:

# 進階版

for n in range(5):
print(" " * (4 - n) + "*" * (2 * n + 1))

print(" " * 4 + "*") # 樹幹
print(" " * 4 + "*") # 樹幹

跟標準版的寫法差不多,只是在最後補上兩行樹幹而已。用 F 字串來寫也很簡單:

# 進階版(使用 F 字串)

for n in range(5):
print(f'{"*" * (2 * n + 1):^15}')

print(f'{"*":^15}') # 樹幹
print(f'{"*":^15}') # 樹幹

最後閃亮亮的豪華版我就不暴雷,留給大家想想看怎麼寫囉 :)

迴圈也有 else?

ifelse 的組合,在 Python 的迴圈也有,只是用起來可能跟你想像的不太一樣。它的寫法是放在跟 for 迴圈同個一層級,但你先猜猜看以下這個迴圈會印出什麼結果:

for n in range(5):
print(n)
else:
print("hello")

執行之後會發現印出 0 ~ 4 之外,在 else 部份的 hello 字串也被印出來了。就以我們在 if...else... 學到的概念來說,這應該是二選一才是,怎麼兩個都印出來呢?這是因為 for 迴圈的 else 運作機制是如果迴圈是「順利走完」的話就會執行。所謂「順利走完」的意思是中間沒有被 break 關鍵字給中斷,如果被中斷的話就不會執行 else,所以像這樣:

for n in range(5):
if n == 3:
break
print(n)
else:
print("hello") # 不會執行

n 等於 3 的時候被 break 中斷了,所以只會印出 0 ~ 2,else 部份也不會執行。

老實說我覺得這樣的設計有點讓人困惑,起因在於我們對 else 的認知是「否則」,就是兩條路給你選一條,所以這裡的 else 其實不算是跟迴圈有關,而是跟迴圈裡的 break 有關,也就是說其實不是 for...else...,硬要說的話更像是 break...else...

不過在某些情況下倒也是挺方便的,例如:

has_money = False

for c in "hello $world":
if c == "$":
has_money = True
break

if not has_money:
print("字串裡沒有 $ 字元")

上面這段範例會檢查字串裡有沒有 $ 字元,我知道這個例子有點呆,因為要檢查有沒有特定的字元有更簡單的函數可以用,根本不用這麼辛苦,這裡只是舉個例子。在這裡我先在迴圈外面設定變數 has_money,如果在迴圈裡轉啊轉的時候發現這裡面有空白字元,就把 has_money 設成 True,然後用 break 中斷迴圈,這樣在迴圈外面就能判斷 has_money 變數來知道是不是有錢人.. 喔,不是,是有沒有 $ 字元。

這如果改用 for...else... 來寫的話,會變成這樣:

for c in "hello $world":
if c == "$":
break
else:
print("字串裡沒有 $ 字元")

這樣就能省掉 has_money 變數,直接在 else 裡面印出訊息就好。但整體來說我還是覺得這設計不太好,上面的例子中如果 else 縮排的位置寫錯,例如縮到跟 if 同個層級並不會造成語法錯誤,但答案就不會是你要的。而且 else 的意思是「否則」,也許這裡用 nobreak 之類的關鍵字會更合適,但 Python 的核心團隊似乎不想再加入新的關鍵字進來所以就挑了現成的 else 來用。其實 Python 的老爸 Guido 後來也公開講過,如果重來一次,他應該不會做這個功能。

不管如何,如果你覺得 for...else... 的寫法更容易閱讀,那就用它吧,只是要記得它的運作機制是跟 break 有關,不是跟迴圈有關。另外,接下來介紹的 while 迴圈,也同樣可以搭配 else 來使用,使用方法跟 for 迴圈的一樣。

while 迴圈

另外一種迴圈是 while 迴圈,它的語法這樣寫:

while 條件判斷:
做一些事情

只要條件成立,也就是判斷的結果是 True,就會一直做裡面的事情,而且會一直做不會停,直到條件判斷變成 False 為止,例如:

while True:
print("Go Go!繼續工作!")

執行這段程式碼的話,就會以非常快的速度不斷的在畫面上印出「Go Go!繼續工作!」,這就是所謂的「無窮迴圈(Infinite Loop)」,直到你按下 Ctrl+C 或是關掉終端機強制終止程式為止。

因此,在 while 迴圈裡通常都會有一段流程來改變條件判斷的結果,不然程式就會一直不斷的執行,舉個例子:

tired = False
angry_level = 0

while not tired:
print("Go Go!繼續工作!")

if angry_level < 10:
angry_level += 1
else:
tired = True
print("我累了,我要離職!")

只要 tired 不是 True,也就是還沒累到不行,這個迴圈就會一直執行,直到憤怒值 angry_level 累積到一定程度之後才會停止這個輪迴。

《練習》猜數字

有玩過猜數字遊戲嗎?就是先從 1 到 100 之間隨便猜一個數字,猜對了就贏了,如果猜錯了會告訴你是猜太大還是猜太小,然後繼續猜直到猜對為止。我們用目前學到的內容來試著寫看看:

import random

answer = random.randint(1, 100) # 隨機產生一個 1~100 的整數
guess = int(input("請猜一個 1~100 的數字:"))

while guess != answer:
if guess > answer:
print("太大了!")
else:
print("太小了!")
guess = int(input("再猜一次:"))

print(f"恭喜你!猜對了!答案是 {answer}")

這裡用到的 random 是 Python 內建的模組,裡面有一個 randint() 函數可以用來產生指定範圍內的隨機整數。接著要注意的是 input() 函數取得的結果都是字串,所以需要用 int() 函數來轉換成整數。這個程式沒有太多的防呆機制,主要的目的是讓大家熟悉迴圈的使用,執行一下,看看你幾次能猜對:

$ python guess_number.py
請猜一個 1~100 的數字:50
太小了!
再猜一次:75
太大了!
再猜一次:60
太大了!
再猜一次:55
恭喜你!猜對了!答案是 55

如果你是用切西瓜的方式對半再對半的方式猜的話,照理說應該最多只要 7 次就能猜到正確的數字了。

迴圈的控制流程

除了順順的讓迴圈跑完,有些時候會需要在迴圈裡面額外做一些控制,例如「跳過這一輪」或是「直接結束迴圈」,Python 有提供一些控制流程的關鍵字讓我們更靈活的控制迴圈的執行流程。先來看看結束迴圈的關鍵字 break,我們就拿剛才寫的猜數字遊戲來改寫:

import random

answer = random.randint(1, 100) # 隨機產生一個 1~100 的整數
guess = int(input("請猜一個 1~100 的數字:"))

while True:
if guess > answer:
print("太大了!")
elif guess < answer:
print("太小了!")
else:
print(f"恭喜你!猜對了!答案是 {answer}")
break; # Bingo! 猜對了!結束迴圈

guess = int(input("再猜一次:"))

我先讓整個 while 迴圈的條件設定成 True,表示這個迴圈會一直進行,然後我在 else 區塊裡面加了一個 break 關鍵字,break 的意思就如它字面上的意思,它會中斷並結束迴圈,不管這時候 while 迴圈的條件判斷結果是什麼,只要遇到 break 關鍵字迴圈就會立刻停止。

for 迴圈也可以用 break 關鍵字來提早結束迴圈,例如:

for i in range(5):
if i == 2:
break

print(i)

這個範例不會跑到最後一圈跑完才結束,而是在 i 等於 2 的時候就提早結束了,所以只會印出 0 跟 1。也許你會好奇,在上面這個範例裡,既然要提早結束為什麼一開始不直接用 range(2) 就好?這就跟人生一樣,照理說我們都希望可以順利的活到 100 歲,但很多時候就是不知道從哪兒蹦出來的突發狀況讓我們沒辦法順利達到目標,就這個 break 一樣,提早結束。

雖然 break 關鍵字會中止迴圈,如果遇到多層迴圈的情況下,break 只會影響的離它最近的那一層迴圈,我用前面介紹過的九九乘法表為例:

for i in range(1, 10):
for j in range(1, 10):
print(f"{i} x {j} = {i * j}")
if j == 2:
break # 休息一下

在內層迴圈裡面的 break 只會影響到自己那層的迴圈,當 j 等於 2 的時候就會中止,但外層還是會繼續執行,所以最後的結果就會變成是:

1 x 1 = 1
1 x 2 = 2
2 x 1 = 2
2 x 2 = 4
... 略 ...
9 x 1 = 9
9 x 2 = 18

另一個跟 break 同樣也可以用來控制迴圈流程的關鍵字是 continuecontinue 字面上的意思雖然是「繼續」,但把它看成「跳過(Skip)」會更貼切,舉個例子:

for i in range(10):
if i % 2 == 0:
continue
print(i)

如果 i 可以被 2 整除(也就是偶數)的時候就執行 continuecontinue 就像是大喊一聲「來,下面一位!」然後結束這一回合。要注意喔,continue 不是結束整個迴圈,而是不管後面還剩下幾行程式,都直接無視然後進到下一回合,所以最後印出來的結果就會是 1、3、5、7、9 這幾個奇數,因為遇到偶數的時候都被跳掉了。

for 迴圈還是 while 迴圈

不管是 forwhile 迴圈,這兩種迴圈都能做到一樣的效果,差別只在好寫或不好寫而已。舉例來說,如果要用 while 迴圈印出 1 ~ 10:

num = 0

while num < 10:
num += 1
print(num)

跟用 for 迴圈比起來就稍微囉嗦一點。如果以使用情境來說,我用跑操場舉個例子,for 迴圈就像是「你去跑 10 圈操場」,而 while 迴圈則是「你去跑操場跑到跑不動為止」。所以當條件明確,例如就是知道要跑 5 次,我會選擇使用 for 迴圈,但如果不確定要跑多少次,要跑到某個條件不滿足為止,我會考慮使用 while 迴圈。

工商服務

想學 Python 嗎?我教你啊 :)

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