第七章 迭代
7-1 notepad++ 的設定
這邊要分享的設定全部都在 notepad++ -> 的「alt 功能表 -> 設定 -> 偏好設定」內
- 一般 -> 關閉最後一個文件時關閉程式
- 不核取是預設值,也就是關完所有用 notepad++ 開的檔案,還會留下 notepad++ 程式,必須按 alt+f4 才會關閉視窗。
- 編輯 -> 換行設定 / 顯示行號
- 若核取顯示行號設定,無論使用點顯器或聽讀都感受不到行號,基本上是給明眼人看的而已。
- 視障者如果真的需要閱讀行號,就去核取 NVDA 功能表 -> 偏好 -> 設定 -> 文件格式 -> 行號,才會有效果。
- 開新文件 -> windows cr / UTF-8 / 套用於 ANSI 檔案
- 關於 windows cr 設定是指在檔案中每行按 enter 時所顯示的是哪一種換行符號,因為我自己的 git 還有網站的正式環境都是 linux, 所以會選擇 unix lf 換行符號,以求統一。
- 如果你全是 windows 環境,就應該選擇 windows cr 設定。
- 關於 UTF-8 中文編碼設定,現在的中文應該都是用 utf-8 編碼了,所以選擇這個項目比較不會有問號字的產生。
- 最近使用的檔案 -> 程式啟動時不檢查 / 僅檔案名稱
- 若選擇「程式啟動時不檢查」,可以加快開檔速度,讓 notepad++ 乾淨地開啟當下指定的那個檔案就好。
- 關於「僅檔案名稱」設定,是指在標題列只顯示檔案名稱,還是連完整的路徑都要顯示。
- 副檔名連結設定
- 完成「副檔名連結設定」後,以後在設定過的副檔名直接按下 enter,即可直接用 notepad++ 來開啟檔案。
- 程式語言 -> 跳格設定 / 使用空格
- 關於「跳格設定」,會作用在按 tab 鍵後,實際上畫面會空幾個空格,通常習慣上是設定 4 個空格。
- 關於「使用空格」,若核取則實際上會摸讀或聽讀到實際空格數,若不核取則是顯示一個 tab 符號替代所有空格。
- 備份 -> 開啟程式時繼續上次的工作階段
- 若核取「開啟程式時繼續上次的工作階段」設定,則開檔時會停在之前存檔的地方。
- 字詞自動完成功能 -> 啟動自動完成功能
- 若核取「字詞自動完成功能」,在輸入時可以有自動推薦完成字詞供選擇,讓你打字時比較省時省力。例如,打到 ran 的 n 時可以讓你用方向鍵選擇 rand, random 等等以 ran 開頭的字,但使用 NVDA 的視障者需要安裝 NVDA 的 notepad++ 附加元件,才能順利選取自動字詞。
- 值得一提的,這些建議的字其實是你輸入過的,或者這個檔案曾經出現過的才會列出來。也可以設定指定自動補齊後括號的功能,避免只打了前括號,卻漏打後括號的問題。
- 這與坊間的 ide 自動建議字詞功能是不同的。ide 是幫你整理這個程式語言中,有哪些類別、函數、方法等可以使用,與檔案本身有沒有出現過相同字詞無關。
- 多重實體開啟 -> 多重實體開啟模式
- 若核取「多重實體開啟模式」,就表示使用 notepad++ 同時開啟多檔後,會使用開新視窗的方式開檔,鍵盤必須使用 alt+tab 來切換不同視窗。
- 不核取的話就是預設值,也就是只有一個 notepad++ 實體,同時開啟多檔後會使用新分頁的方式開檔,鍵盤必須使用 ctrl+tab 來切換不同分頁。
- 其他
- 在這一區還有一些設定項目都值得大家去研究看看,notepad++ 是我們常用的軟體,應該了解一下設定比較好。
7-2 安裝 notepad++ 附加元件
使用 NVDA 的視障者有需要 自動補齊 功能就一定要安裝。
當然還有其他附加功能,上面的設定雖然打勾了,但在操作那個補齊介面時,原生 NVDA 無法支援。
詳細的附加元件操作可以看這篇 Notepad++ 編輯器增強 附加元件
7-3 檔案編碼
使用 notepad++ 開啟檔案應該是 utf-8 編碼,但更保險一點的做法是開新檔時自己選擇一次 utf-8 編碼。
特別是使用 notepad++ 去開啟之前建立或當初是以記事本儲存的檔案,都要注意一下中文編碼的問題:
- alt 功能表
- 往右找到「編碼」
- 往下找到「轉換至 UTF-8 碼格式」按 enter
只要在第一次開檔時做一次就可以了,我在撰寫程式時很少建立新檔,通常都是複製之前舊的 utf-8 檔案,清空內容改名後開始寫程式,就不需要再做這個設定。建議大家都用 utf-8 編碼來撰寫程式,比較不會出現中文亂碼問題。
7-4 什麼是迭代
迭代就是重複執行一個程式碼區塊敘述的過程。實現迭代可以有很多方式,我們之前看過了 遞迴 與 for 迴圈。
接下來我們要看一下 while 迴圈。但在這之前,我們先來說一下關於賦值給變數的一些觀念。
7-5 重新賦值
你可能已經發現,在 python 中可以重複賦值給同一個變數。
當你賦一個新值給已存在的變數,同時該變數也會隨著新值改變其資料型態。
>>> x = 5
>>> x
5
>>> type(x)
<class 'int'>
>>> x = 'abc'
>>> x
'abc'
>>> type(x)
<class 'str'>
一開始,我們給 x 變數 5, 後來又改成 'abc'。讓我們來釐清一下等號給我們的一些錯覺。
在 python 中使用 = 來賦值,也就是把右邊的值指派給左邊,僅僅如此而已。
不要把等號跟數學的等於完全聯想在一起,有時候它們不太一樣。
首先,對數學而言,等於代表兩邊都相等,
而在 python 中,x = 7 但 7 = x 是錯誤的語法。
再來,對數學而言,等於是恆等式,可是 python 不一定。看一下這個:
>>> a = 5
>>> b = a # b 和 a 相等
>>> a = 3 # b 和 a 不再相等
>>> b
5
從上面的例子可以看出,我們改變 a 的值,但並不會順便把 b 都改了,所以 a 不恆等於 b。
它們兩個變數的記憶體位置是不同的,所以雙方的值並不會互相影響,把等號理解成將左邊這個標籤指向右邊的值會比較好。
也就是說,右邊的常數假設都存在於這個世界中,我們只是暫時把左邊的標籤貼給右邊的值而已。
右邊的值可以同時被貼很多張標籤,而標籤也可以隨時撕下來改貼到別的地方去。
雖然重複指派不同的值給同一個變數有時很方便,但必須小心使用這樣的方式,
因為這樣做可能讓你的程式不容易理解或閱讀,並且增加了發生錯誤的可能性,在除錯上也比較困難。
def sum(a):
for i in range(a):
print(a)
a+=i
return a
print(sum(10))
7-6 變數更新
常見的重複賦值情況,是參考原本的值後更新變數的新值,如下:
>>> x = 0
>>> x = x + 1
等號的優先權其實很低,先看等號右邊,將 x 的值加一後,把新值再次指派給 x。
如果你對一個不存在的變數更新其值,你會得到一個錯誤,因為 python 會先檢查等號左邊,發現 y 並不存在。
>>> y = y + 1
NameError: name 'y' is not defined
所以,更新變數的值時,一定要先初始化這個變數,通常我們會簡單地這樣做:
>>> x = 0
>>> x = x + 1 # 或 x += 1
循環增加叫做 遞增 ,而 循環減少就叫做 遞減。
7-7 while 敘述
電腦擅長重複執行任務,且結果非常可靠,這樣的動作又稱為 迭代。
迭代是很普遍的行為,python 很容易做到,一個是 for 迴圈,這個我們稍後會再深入探討,
另外一個就是 while 迴圈了,以下這是之前第五章 countDown 函數的 while 版本:
def countDown(n):
while n > 0:
print('還有', n, '秒會爆炸')
n = n - 1
print('爆炸')
當 n 大於 0 時就列印「還有 n 秒會爆炸」,並遞減,直到 不大於 0 時,進入迴圈的條件為假,所以印出 爆炸。
以下是 while 迴圈的執行流程:
- 確定進入迴圈的條件是 True 還是 False
- 條件為 False 時跳過整個 while 區段並執行 while 區塊之外的敘述。
- 條件為 True 時進入 while 迴圈執行其程式區塊,然後返回第一個步驟循環,直到條件為 False。
這種運作方式被稱為 迴圈,因為執行完第三步又會回到第一步。
在迴圈的本體會試著改變多個變數的值,使其進入迴圈的條件最終為 False,
這樣才能離開迴圈的執行,否則將成為無限的循環,這稱為 無窮迴圈。
對於 countDown 函數,我們每次在 while 迴圈本體中減一,直到它等於 0 或小於 0 時離開迴圈。
但有些 while 迴圈的條件,不容易分辨是否一定不會造成無窮迴圈:
def sequence(n):
while n != 1:
print(n)
if n % 2 == 0: # n 是偶數
n = n / 2
else: # n 是奇數
n = n * 3 + 1
上面的函數,當 n 不等於 1 就進入迴圈;而當 n 等於 1 ,則條件為 False ,不執行 while 迴圈。
每次進入迴圈會先印出 n 的值,讓我們便於確認現在 n 的值,最主要是想知道 n 是偶數還是奇數。
如果是偶數,n 就除以 2 ;如果是奇數,n 就乘以 3 再加 1。
我們使用參數 3 帶入 sequence ,得到的所有 n 值如下:
3, 10, 5, 16, 8, 4, 2, 1
從這個案例中,由於 n 有時遞增又有時遞減,因此我們很難一下子就斷定 n 最後是否一定會等於 1。
但以這個例子,當 n 為某些值時,我們能證明它最後一定會回到 1 這個值。例如 n 是 2 的次方,且每次除以 2 後都會得到偶數。
困難的是,目前這世界上還沒有人能夠證明 n 為正數時,最後都能回到 1 上。
總之,想使用這樣的判斷式當作進入迴圈與否的條件要很小心,必須確保不要造成無限循環。
7-8 break 中斷敘述
離開迴圈的方式,除了進入迴圈判斷式的值為 False 外,還可以在迴圈中設定達到某個條件,就強制跳開迴圈,
如果是後者,必須使用 if 搭配 break 敘述。
舉個例子,假設你希望讓使用者一直輸入,直到輸入 done 才離開迴圈,進行後續動作時,可以這樣寫:
while True:
line = input('輸入 down 離開: ')
if line == 'done':
break
print(line)
因為 while 的進入條件為 True ,所以一定會進入該迴圈,讓使用者透過 input 函數輸入任何文字,
如果不是輸入 done ,就印出其內容,並返回迴圈頂部再度循環。
若輸入 done ,則執行 break 敘述中斷迴圈,並執行迴圈之外的其他敘述。
這樣的寫法很常見,使你可以直接進入 while 迴圈,想離開時可以隨時在迴圈的任何地方加上 break 的條件判斷,
令你的 while 迴圈條件變得絕對,像是利用這種方式讓使用者透過輸入某個關鍵字來結束輸入。
但請切記,使用這樣的迴圈一定要加入中斷條件,否則程式將進入 無窮迴圈 而無法停止。
7-9 continue 跳回敘述
在迴圈當中,如果某種條件成立後,我們希望跳過後面的敘述,直接回到迴圈的判斷條件再繼續執行。
這樣的需求可以使用 if 搭配 continue 敘述來完成。
假設有一個自動改選擇題作業的程式,答對時給10分,答錯時則印出錯誤的答案且不給分數,看看以下範例:
範例 7-1
answers = 'cbadcbdabc' # 正確答案
answer = 'cbdacddbbc' # 學生的答案
num = sum = 0 # 題數與總分
print('正確答案是', answers)
for num in range(len(answers)):
if answer[num] != answers[num]: # 答錯就印出錯的答案且回到迴圈開頭
print('第 ', num + 1, '題答錯:', answer[num])
continue
sum += 10 # 答對就累加 10 分
print('得到 ', sum, '分') # 迴圈結束後印出總得分
7-10 演算法
演算法是一種計算的方法,好的演算法可以說是一種速算的方法。
像九九乘法表就不是一種演算法,因為我們是直接記下它的答案。
舉例來說,某個數加上 9 可能不好算,如果你想偷懶,可以先算加 10 再減 1,反而會比較快。
總之,推導演算法很無聊,但設計演算法很有趣,可以說是一種智力挑戰。
若能找出規則而設計出一些演算法,對於撰寫程式與提升程式效能很有幫助。
7-11 迴圈也有 else 敘述
我們已經知道,在 if 敘述可以使用 else 來當 if 條件為 False 時執行的程式區塊。
事實上迴圈也可以使用 else 敘述,不管是 for 還是 while 都適用。
其實它與在 if 時使用的條件一樣,也就是當迴圈進入條件為 False 時,會離開迴圈並執行 else 的程式區塊。
要注意的是,迴圈進入條件的第一圈判斷為 False ,也會執行 else 的部分。反過來說,當迴圈中使用了 break 強制離開,便不會去執行 else 的部分。
使用的時機是當迴圈條件為 False 需要做某些事,就能用 else 敘述。要特別提醒的是,迴圈順順跑完,最後條件亦會變成 false ,所以也會去執行 else 內的程式碼。
以下是一個練習英文單字遊戲的程式片段,如果單字中含有3個以上(包含3個) 的 aeiou 母音字母,就會印出錯誤訊息,反之則出現通過訊息。
但不管有幾個母音字母,最後都會印出遊戲結束的字樣。
sum = 0
for letter in word:
if letter == 'a' or letter == 'e' or letter == 'i' or letter == 'o' or letter == 'u':
sum += 1
if sum > 2:
print('該單字超過2個母音字母')
break
else:
print('通過')
print('遊戲結束')
我們這邊只是舉例使用迴圈與 else 的時機,先不管是否有更好的處理方式。使用 word 變數來帶入一些單字,測試結果。
程式遍歷整個 word 字串,當 word 裡的字母出現 a, e, i, o, u 其中一個,就累加 sum 變數的值。
迴圈的結尾會判斷 sum 是否已經大於 2 了,如果是就印出錯誤訊息並強制離開迴圈。這樣的情況下,迴圈的 else 不會被執行到。
如果遍歷完 word 而 sum 沒有超過 2, 就表示這個單字沒有超過規定的母音字母數量,
此時會執行 else 的程式區塊,也就是印出通過訊息。
當遍歷完 word 就表示 for 迴圈的條件為 False, 因此會執行 else 程式區塊。
如果是空字串的話,也會通過。
除錯
隨著撰寫的程式越來越大,發生錯誤的機率與次數也越來越多,花在除錯的時間也會變長。
這裡介紹一個除錯的方法,叫做 二分除錯法。
當你寫了一個 100 行的程式,每次都從第 1 行開始除錯,則需要 100 個步驟來完成這件事。
不妨考慮把問題分成兩邊,在中間進行除錯,例如印出變數的值,或利用除錯工具,
如果發現有錯,問題可能發生在前 50 行,不然就測試後半部分,以此類推。
重複做了 6 次左右,就可以很精確地把問題點縮小到數行的程式碼間。
不過,直接從中間的行數去尋找錯誤點是不實際的。
比較好的方式是根據你的程式來思考,尋找可能發生錯誤的地方,加以確認。例如某段程式碼的運算比較複雜,出錯的機率就相對較高。
當程式有問題時,應該從最有可能的地方下手除錯,效率比較高。
動動腦
1.思考以無窮迴圈的設計方式,如何求出一個數的正有理數平方根。(不能使用任何函數)
number = 25000000
a = 1
while True:
if a ** 2 == number:
print(number, '的有理數平方根為', a)
break
if a > number:
print(number, '沒有正有理數的平方根')
break
a += 1
print('遊戲結束')
2.同樣功能的程式往往因為撰寫者的思考方式不同,會有很多種寫法,這也是程式有趣的地方之一。
上例計算選擇題答案的程式範例 7-1,試著改寫成不使用 continue 與 break 的版本看看。
answers = 'cbadcbdabc' # 正確答案
answer = 'cbdacddbbc' # 學生的答案
num = sum = 0 # 題數與總分
print('正確答案是', answers)
for num in range(len(answers)):
if answer[num] == answers[num]: # 答對就累加 10 分
sum += 10 # 答對就累加 10 分
else: # 答錯就印出錯誤的答案
print(f'第 {num + 1} 題答錯: {answer[num]}')
print('得到 ', sum, '分') # 迴圈結束後印出總得分
練習
第1題 檔名 7-1.py
有個內建函數名為 eval,它的功能是將字串的算式進行一般算式的演算並返回答案。像這樣:
>>> eval('1 + 2 * 3')
7
>>> import math
>>> eval('math.sqrt(5)')
2.2360679774997898
>>> eval('type(math.pi)')
<class 'float'>
寫一個函數名為 evalLoop, 功能是讓使用者一直輸入算式,每次都用 eval 計算出結果並印出來,
直到使用者輸入 quit 才離開。
第2題 檔名 7-2.py
寫一個函數,接受一個整數參數,功能是列出這個數的標準分解式。
至少可以做到這樣:
>>> factorization(100)
2*2*5*5
進階練習就是整理上面的式子成為真正的標準分解式,但這部分會有一點難度,有興趣的人可以挑戰一下:檔名 7-2p.py
>>> factorization(100)
2^2 * 5^2
我們這邊先暫時以 ^ 代表次方的符號。
第3題 檔名 7-3.py
寫一個讓使用者猜三位數的猜數字遊戲,先將答案設定成一個固定的值,例如 312,
接著開始讓使用者輸入答案,如果輸入的不是三位數就印出提醒,猜錯也要印出太大或太小的提示,並且持續讓使用者猜到答對為止。
唯一的規定是,一定要用無窮迴圈的方式來撰寫這個程式。
參考資料
影片
最後更新:2021-10-08 21:12:31
From: 111.249.165.250
By: 特種兵