[python] [VI coding] 第四章 案例研究:介面設計 - 教學區

[python] [VI coding] 第四章 案例研究:介面設計

文章瀏覽次數 2420

特種兵

特種兵圖像(預設)

2020-08-21 12:21:58

From:211.23.21.202

第四章 案例研究:介面設計

4-1 結合開發環境

目前我們每次在檔案中寫完程式,都必須進指令列(command line)下執行程式,也就是腳本模式。現在我們要把 notepad++ 跟 python 直譯器結合在一起,也就是在 notepad++ 寫完程式,就可以按一個快速鍵來執行程式,馬上看到程式執行結果,這有助於我們修改程式並大大增加開發程式的效率。

它的原理其實很簡單,一樣是開另一個視窗進命令列,然後呼叫 python 過來直譯我們的程式,最後把結果顯示在畫面上。

  1. 開啟 notepad++
  2. 按 F5 呼叫執行視窗
  3. 貼上 cmd /k cd /D "$(CURRENT_DIRECTORY)" & python "$(FILE_NAME)" & pause & exit
  4. 按 tab 到儲存並按 enter
  5. 再設定快捷鍵視窗核取 ctrl
  6. 使用 tab 鍵到下拉式方塊,按下方向鍵選擇 F6 (其他按鍵也可以,不要與原本的功能衝突即可)
  7. 按 tab 到編輯區輸入一個名稱,如: python
  8. 按 tab 到 確認 並按 enter
  9. 按 esc 鍵離開執行視窗
  10. 用 notepad++ 開啟副檔名為 .py 的檔案,按 ctrl+F6 確認是否有執行結果

4-2 安裝 pip

pip 是一個可以幫 python 安裝外部模組的程式,安裝以後,就可以像我們上一個章節描述的那樣載入模組,並使用模組內的函數。

使用 pip 安裝模組的過程,可以想成是從網路上下載那些模組的程式碼,放在 python 能搜尋到的路徑當中,然後提供程式去調用它們。

我們先來確認目前 python 有沒有安裝 pip 了(較新版本的 python 會內建):

  1. 按 win + r 開啟執行列
  2. 輸入 cmd 按 enter 進入 windows 命令列視窗
  3. 輸入 cd\python376\Scripts 按 enter 切換到該資料夾下。(看看當時安裝 python 的路徑在哪裡)
    c:\python376 是我的 python 安裝路徑,預設的有可能是 `c:\Users\administrator\AppData\Local\Programs\Python\Python37(而 administrator 是我的電腦使用者帳號)
  4. 輸入 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 這個程式了:

  1. 在剛剛的資料夾下,輸入 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 畫圖

讓我們練習寫幾個函數來畫簡單的圖形。

字母畫框

先來利用字母畫一個正方形框,歸納需要的函數條件如下:

  1. 需要一個函數負責畫縱向的圖形
  2. 需要一個函數負責畫橫向的圖形
  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 開發計畫

規劃好程式再進行撰寫是很重要的。在前面我們練習怎麼思考並找出相同點與不同點,來完成程式或函數。

我們把這些過程歸納一下:

  1. 先寫一個沒有函數的程式,完成它的功能
  2. 程式的功能測試正常後,試著找出一些相同的程式碼,將它提取出來改寫成函數
  3. 改寫函數後,介面設計乾淨,讓原本的程式可以順利調用,其他有需要的程式也行
  4. 調整你的函數設計,避免重複的程式碼,讓整個輸出與輸入更邏輯化,並讓函數更有應用性
  5. 有機會時改進程式或函數,如果多個地方出現重複的程式碼,應該考慮分解到合適的通用函數中(重構)

至少透過上述的練習,可以讓我們的整個程式更模組化,邏輯性也更強,簡單地說就是更專業,看起來也更像程式的感覺。

4-11 說明字串(docstring)

這個說明字串主要用途是解釋函數,也可以說,就是函數一開始的區塊註解。

我們之前提過單行註解,還有多行註解,當時提過三個引號包夾被利用來當作多行註解,實際上它是 docstring 功能,簡要說明函數用途、參數的意義與形態,讓程式設計師快速了解是不是要調用或改寫這個函數。

以下是一個函數,我們使用 docstring 來撰寫註解:

def errorDialog(message, this_text, caption):
	''' 顯示對話框的設計 適用整個程式需要彈出訊息時
	message 在視窗中顯示的訊息文字
	this_text 當該視窗關閉後會將焦點移到這個元件上
	caption 該對話框的標題文字 '''

不過先不實作這個函數,因為目前超出我們的能力。

除錯

介面是函數與呼叫者之間的契約與橋樑。

呼叫者根據介面的描述提供正確的參數,而函數根據呼叫者的需求完成功能。

就像剛才的 errorDialog 函數,它有三個參數:

  1. message 是一個字串
  2. this_text 是一個元件
  3. 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題

試著利用本章所學,優化本章範例。

  1. 使用 for 迴圈改寫 traverseLine 函數,並測試功能是否正常。檔名 4-1.py
  2. 使用 for 迴圈改寫 straightLine 函數,並測試功能是否正常。檔名 4-2.py
  3. 利用變數來改寫 4-3-1 讓程式更靈活方便。檔名 4-3.py
第2題 檔名 4-4.py

使用 for 迴圈修改第二章的習題,原本是求 1 到 10 的和,換成帶入 n 可以算出 1 到 n 總和的函數。

參考資料

關於 python 的泛型討論

影片

第四章 案例研究-介面設計 part one

第四章 案例研究-介面設計 part two

最後更新:2021-10-08 22:26:38

From: 111.249.165.250

By: 特種兵