第四章 案例研究:介面設計
4-1 結合開發環境
目前我們每次在檔案中寫完程式,都必須進指令列(command line)下執行程式,也就是腳本模式。現在我們要把 notepad++ 跟 python 直譯器結合在一起,也就是在 notepad++ 寫完程式,就可以按一個快速鍵來執行程式,馬上看到程式執行結果,這有助於我們修改程式並大大增加開發程式的效率。
它的原理其實很簡單,一樣是開另一個視窗進命令列,然後呼叫 python 過來直譯我們的程式,最後把結果顯示在畫面上。
- 開啟 notepad++
- 按 F5 呼叫執行視窗
- 貼上
cmd /k cd /D "$(CURRENT_DIRECTORY)" & python "$(FILE_NAME)" & pause & exit
- 按 tab 到儲存並按 enter
- 再設定快捷鍵視窗核取 ctrl
- 使用 tab 鍵到下拉式方塊,按下方向鍵選擇 F6 (其他按鍵也可以,不要與原本的功能衝突即可)
- 按 tab 到編輯區輸入一個名稱,如:
python
- 按 tab 到 確認 並按 enter
- 按 esc 鍵離開執行視窗
- 用 notepad++ 開啟副檔名為 .py 的檔案,按 ctrl+F6 確認是否有執行結果
4-2 安裝 pip
pip 是一個可以幫 python 安裝外部模組的程式,安裝以後,就可以像我們上一個章節描述的那樣載入模組,並使用模組內的函數。
使用 pip 安裝模組的過程,可以想成是從網路上下載那些模組的程式碼,放在 python 能搜尋到的路徑當中,然後提供程式去調用它們。
我們先來確認目前 python 有沒有安裝 pip 了(較新版本的 python 會內建):
- 按 win + r 開啟執行列
- 輸入 cmd 按 enter 進入 windows 命令列視窗
- 輸入
cd\python376\Scripts
按 enter 切換到該資料夾下。(看看當時安裝 python 的路徑在哪裡)
c:\python376
是我的 python 安裝路徑,預設的有可能是 `c:\Users\administrator\AppData\Local\Programs\Python\Python37(而 administrator 是我的電腦使用者帳號) - 輸入 pip 並按 enter 確認是否有該程式
往上看,如果沒有正確的執行結果,就在該資料夾下進行安裝。
所謂正確的執行結果,就是會列出 pip 這個指令的參數,而沒有錯誤訊息,類似這樣:
-v, --verbose Give more output. Option is additive, and can be used up to 3 times.
-V, --version Show version and exit.
-q, --quiet Give less output. Option is additive, and can be used up to 3 times
(corresponding to WARNING, ERROR, and CRITICAL logging levels).
--log <path> Path to a verbose appending log.
--no-input Disable prompting for input.
--proxy <proxy> Specify a proxy in the form [user:passwd@]proxy.server:port.
--retries <retries> Maximum number of retries each connection should attempt (default 5
times).
--timeout <sec> Set the socket timeout (default 15 seconds).
--exists-action <action> Default action when a path already exists: (s)witch, (i)gnore, (w)ipe,
(b)ackup, (a)bort.
--trusted-host <hostname> Mark this host or host:port pair as trusted, even though it does not
have valid or any HTTPS.
--cert <path> Path to alternate CA bundle.
--client-cert <path> Path to SSL client certificate, a single file containing the private
key and the certificate in PEM format.
--cache-dir <dir> Store the cache data in <dir>.
--no-cache-dir Disable the cache.
--disable-pip-version-check
Don't periodically check PyPI to determine whether a new version of pip
is available for download. Implied with --no-index.
--no-color Suppress colored output.
--no-python-version-warning
Silence deprecation warnings for upcoming unsupported Pythons.
--use-feature <feature> Enable new functionality, that may be backward incompatible.
--use-deprecated <feature> Enable deprecated functionality, that will be removed in the future.
如果有錯誤訊息,例如系統找不到這個指令,就必須先進行安裝 pip 這個程式了:
- 在剛剛的資料夾下,輸入
easy_install pip
按 enter ,即可安裝完成
建議將上面提到的 python Scripts 的絕對路徑也加到系統 path 中,以後要使用 pip 安裝程式,就不用進到那麼深的資料夾了。
用 pip 安裝模組
輸入 pip install 模組名稱 可以安裝模組,如果你想移除與更新模組,輸入 pip 按 enter 可以看到指令操作說明。
而輸入以下指令,可以更新 pip 這個程式:
> python -m pip install --upgrade pip
Successfully installed pip-19.0.2
有時在利用 pip 安裝模組時,系統會提醒 pip 太舊,需要更新的警告。
4-3 畫圖
讓我們練習寫幾個函數來畫簡單的圖形。
字母畫框
先來利用字母畫一個正方形框,歸納需要的函數條件如下:
- 需要一個函數負責畫縱向的圖形
- 需要一個函數負責畫橫向的圖形
- 分別給定兩個參數,一是字母(字串),二是數量(整數)
最直覺的就是思考函數會怎麼執行:
>>> straightLine('A', 5)
A
A
A
A
A
>>> traverseLine('A', 5)
AAAAA
理論上,有橫線也有直線,就可以畫出方形了。
那麼想畫出一個邊長為 5 個字母的正方形,應該怎麼執行函數?
如果我們先畫了上面的橫線,接著畫左邊的直線,那右邊的直線怎麼回去畫?
目前,就我們所學,必須先按照畫面的順序來思考這個問題,也就是從上到下,由左而右。
# 第一排 先畫上面的橫線
traverseLine('A', 5)
# 第二排 第一組,最左邊與最右邊各一個直線,中間需要空格才能對齊
traverseLine('A', 1)
traverseLine(' ', 3)
traverseLine('A', 1)
# 我們共需要三組一樣的畫法
# 第三排 第二組
traverseLine('A', 1)
traverseLine(' ', 3)
traverseLine('A', 1)
# 第四排 第三組
traverseLine('A', 1)
traverseLine(' ', 3)
traverseLine('A', 1)
# 第五排 最後是下面的橫線
traverseLine('A', 5)
最後編寫函數,以這個案例來說,我們發現,實際上根本用不到畫直線的函數。
def traverseLine(ch, num):
print(ch * num)
程式執行看看,會發生什麼事:
AAAAA
A
A
A
A
A
A
AAAAA
我們的第二排到第四排有問題,原因是 print 執行完會自動換行,導致我們的字母無法對齊。
但如果限制不要換行,那該換行的地方又無法換行了。
我們暫時採取最簡單的方式,也就是在自訂函數中的 print 使用完畢都不換行,想換行時再自己加入換行符號。
最後,完整的程式定義與執行如下:
# 編號 4-3-1 習題會用到
# 用字母來畫橫線
def traverseLine(ch, num):
print(ch * num, end = '')
# 第一排 先畫上面的橫線
traverseLine('A', 5)
# 第二排 第一組,最左邊與最右邊各一個直線,中間需要空格才能對齊
print()
traverseLine('A', 1)
traverseLine(' ', 3)
traverseLine('A', 1)
# 我們共需要三組一樣的畫法
# 第三排 第二組
print()
traverseLine('A', 1)
traverseLine(' ', 3)
traverseLine('A', 1)
# 第四排 第三組
print()
traverseLine('A', 1)
traverseLine(' ', 3)
traverseLine('A', 1)
# 第五排 最後是下面的橫線
print()
traverseLine('A', 5)
執行結果:
AAAAA
A A
A A
A A
AAAAA
終於完成囉。
第一個參數可以讓我們任意選擇字母來畫正方形框。
第二個參數就是想畫的數量。
字母畫 L
一樣按照由上到下,從左而右的順序,呼叫函數與執行狀況,希望像這樣:
>>> straightLine('A', 4)
A
A
A
A
>>> traverseLine('A', 5)
AAAAA
畫橫線的函數已經在上個案例完成,現在只要撰寫畫直線的函數。
畫直線的函數,每次畫一個字母就需要自動換行,但 print 結束後還需要印橫線,所以不希望換行:
def straightLine(ch, num):
print((ch + '\n') * num, end = '')
這個案例比前一個容易,完整程式碼如下:
# 用字母來畫直線
def straightLine(ch, num):
print((ch + '\n') * num, end = '')
# 用字母來畫橫線
def traverseLine(ch, num):
print(ch * num, end = '')
straightLine('A', 4)
traverseLine('A', 5)
在習題中會讓大家優化這個程式。
4-4 重複執行
我們希望程式可以自動完成重複的工作,它們做起來往往比人更可靠,這也是我們需要電腦或機器的原因。
像上面的繪圖,我們想畫一條長 10 格的短橫線 -
會這樣帶參數到函數:
>>> traverseLine('-', 10)
----------
事實上我們可以用 for 語法敘述,讓程式更簡潔且更具彈性,以下是 python 的 for 迴圈範例:
for i in range(4):
print('Hello!')
我們可以看到如下結果:
Hello!
Hello!
Hello!
Hello!
這是 for 迴圈的最簡單用法,有了迴圈,我們可以把上面的繪圖函數改寫成迴圈的版本。
或者利用迴圈來畫圖,而不是一直手動執行函數做相同的動作。
for 迴圈的寫法很像函數的定義,迴圈內容區塊需要縮排,就像函數內容一樣,使用冒號作為定義與內容的分隔符號。
i 是一個變數,當然不一定要使用這個名字。range 是一個內建函數,裡面放置你想要的執行次數。
我們將上面的 for 迴圈例子改寫一下,觀察 i 的值:
for i in range(4):
print(i)
執行結果:
0
1
2
3
i 會從 0 開始,每一圈增加 1 直到 range 裡的數值減 1 為止。
以上例來說,就是 0, 1, 2, 3 共 4 圈。
我們會慢慢發現迴圈的好處,它有更多的應用情境,我們之後再來探討。
4-5 畫星星
我們現在利用 for 迴圈與 print 函數來畫星星圖,寫一個函數名為 star ,裡面有一個數字參數,它可以幫我們畫出星星圖,像這樣:
star(3)
*
**
***
star(5)
*
**
***
****
*****
通常我們會先觀察,確認目標後再進行程式撰寫,最後測試並完成作品。
- 每行的星星數就是行號
- 星星數每一行都會增加一個
- 星星都從每一行的最左邊開始列印
def star(n):
for i in range(n):
print('*' * (i + 1))
star(5)
我們完成了基本款,接下來挑戰進階版的 superStar() 函數
superStar(3)
*
**
***
**
*
- 第一部分與之前的 star 函數一樣
- 第二部分,星星數每一行就遞減一個,直到最後一個
for 迴圈可以有兩個參數,第一個參數是迴圈起始數值,第二個參數與原本相同。
for 迴圈可以有三個參數,第三個參數是遞減或遞增值。
def superStar(n):
for i in range(1, n + 1):
print('*' * i)
end = n - 1
for i in range(end, 0, -1):
print('*' * i)
superStar(5)
最後是畫出三角形的星星圖 triangle() 函數
triangle(4)
*
***
*****
*******
- 星星數每一行會增加兩個
- 第一行的星星只有一顆,其空格數量是總星星數減一個,也就是空6格,逐行遞減直到沒有
def triangle(n):
sp = ' '
ch = '*'
s = -1
for i in range(n - 1, -1, -1):
s += 2
print(sp * i + ch * s)
triangle(5)
4-6 封裝
在函數中包裝一段程式碼稱為封裝,好處是重新使用程式碼只需調用一次函數,會更簡潔,也比單純的手動複製、貼上好得多。封裝在函數內也可以變成一個模組,讓其他程式引入並調用。
例如我們寫了一個名為 square 的函數,裡面調用其他繪圖的函數,實現了畫出正方形的功能,這就是函數的封裝概念。
4-7 介面
這裡的介面是指函數的設計而言,一個函數的功能、參數與回傳值這些訊息,就是函數的介面設計。
一個良好的函數會避免做不相干的事情,簡而言之就是一個函數做一件事,盡量把函數功能拆分清楚。
這樣在不同的情境下,這些函數才有重複被利用、被組合的價值。
例如你設計了一個畫正方形的函數,裡面順便把畫對角線的功能加進去,這樣的設計在這個需要畫對角線的應用當中或許很方便,但如果下次想畫的是兩個正方形構成的長方形,而不需要畫對角線呢?
所以比較好的函數介面設計,是畫正方形一個函數,畫五角形一個函數,畫對角線一個函數。
這樣下次不管畫什麼形,如果還要加上對角線,只要選擇畫什麼形的函數,再加上畫對角線的函數就完成了。
4-8 泛型
目前先不討論它,我們先著重實作的部分,關於這部分的討論,可以參考講義後面的 參考書目
4-9 重構
重新整理一個程式以改進函數介面和促進程式碼重複利用的過程,被稱為重構。
例如你設計了一個畫正方形的函數,但現在想要畫一個菱形,兩者之間可能有些特性是相同的,不過還是有差異。我們通常會拿寫好的畫正方形函數,修改成畫菱形的函數,會更省事,這就是重構。
一開始撰寫程式,我們往往無法考慮到那麼完全,經常是針對已經有的程式,依據需求做一些修改。重構的過程會讓我們學到更多,以後隨著經驗累積,在規劃函數或程式上會更靈活、通用且完整。起初可能是想到什麼寫什麼,最後雖然完成功能,但程式碼不好閱讀且冗長,如果可以重構它,就表示你有在成長,功力也隨之提升。
4-10 開發計畫
規劃好程式再進行撰寫是很重要的。在前面我們練習怎麼思考並找出相同點與不同點,來完成程式或函數。
我們把這些過程歸納一下:
- 先寫一個沒有函數的程式,完成它的功能
- 程式的功能測試正常後,試著找出一些相同的程式碼,將它提取出來改寫成函數
- 改寫函數後,介面設計乾淨,讓原本的程式可以順利調用,其他有需要的程式也行
- 調整你的函數設計,避免重複的程式碼,讓整個輸出與輸入更邏輯化,並讓函數更有應用性
- 有機會時改進程式或函數,如果多個地方出現重複的程式碼,應該考慮分解到合適的通用函數中(重構)
至少透過上述的練習,可以讓我們的整個程式更模組化,邏輯性也更強,簡單地說就是更專業,看起來也更像程式的感覺。
4-11 說明字串(docstring)
這個說明字串主要用途是解釋函數,也可以說,就是函數一開始的區塊註解。
我們之前提過單行註解,還有多行註解,當時提過三個引號包夾被利用來當作多行註解,實際上它是 docstring 功能,簡要說明函數用途、參數的意義與形態,讓程式設計師快速了解是不是要調用或改寫這個函數。
以下是一個函數,我們使用 docstring 來撰寫註解:
def errorDialog(message, this_text, caption):
''' 顯示對話框的設計 適用整個程式需要彈出訊息時
message 在視窗中顯示的訊息文字
this_text 當該視窗關閉後會將焦點移到這個元件上
caption 該對話框的標題文字 '''
不過先不實作這個函數,因為目前超出我們的能力。
除錯
介面是函數與呼叫者之間的契約與橋樑。
呼叫者根據介面的描述提供正確的參數,而函數根據呼叫者的需求完成功能。
就像剛才的 errorDialog 函數,它有三個參數:
- message 是一個字串
- this_text 是一個元件
- caption 是一個字串
功能都已經在註解中說明,當然也可以把形態寫進去。
以上這些要求稱為 前置條件,因為它必須在函數執行前確保成立。
相對的,另外一個稱為 後置條件,也就是函數結束前帶來的效果,例如畫線、畫圖、移動或任何改變等行為。
因為前置條件由呼叫者提供,當前置條件不正確時產生的錯誤,需由調用函數的人負責。反之,如果呼叫者確實依循函數介面提供符合條件的參數,但結果不如預期時,則函數開發者必須負責。
接著,舉兩個在撰寫程式縮排時容易遇到的典型錯誤:
# 敘述屬於 for 迴圈內,該縮排而未縮排
for i in 'hello':
print(i)
# IndentationError: expected an indented block
# 敘述應該在同一層級區塊,不該縮排卻縮排
s = 'hello'
print(s)
# IndentationError: unexpected indent
動動腦
1.架構程式的一個方式,撰寫虛擬碼,以 猜三位數 為例,讓我們寫寫看。
設定答案
進入迴圈:
讓使用者輸入所猜的數字
如果 使用者猜太大:
提示使用者要猜小一點
那如果 使用者猜太小:
提示使用者猜大一點
上面兩個如果都沒發生的話
提示使用者猜對了
離開迴圈
2.在 print() 中使用 + (加號) 與 , (逗號) 的差別是什麼?
ch = 'hello'
print(ch, '\n')
print(ch + '\n')
print((ch, '\n') * 3)
print((ch + '\n') * 3)
- 個數的不同
- 意義的不同
練習
第1題
試著利用本章所學,優化本章範例。
- 使用 for 迴圈改寫 traverseLine 函數,並測試功能是否正常。檔名 4-1.py
- 使用 for 迴圈改寫 straightLine 函數,並測試功能是否正常。檔名 4-2.py
- 利用變數來改寫 4-3-1 讓程式更靈活方便。檔名 4-3.py
第2題 檔名 4-4.py
使用 for 迴圈修改第二章的習題,原本是求 1 到 10 的和,換成帶入 n 可以算出 1 到 n 總和的函數。
參考資料
影片
最後更新:2021-10-08 22:26:38
From: 111.249.165.250
By: 特種兵