[python] [VI coding] 第十六章 類別與函數 - 教學區

[python] [VI coding] 第十六章 類別與函數

文章瀏覽次數 1007

特種兵

特種兵圖像(預設)

2021-10-08 20:32:50

From:111.249.165.250

第十六章 類別與函數

在知道如何定義類別型態後,這一章試著定義跟類別有關的函數、相關參數與返回值。

先繼續使用函數式風格撰寫程式,並試著討論兩個專案計畫的範例。

16-1 時間

定義名為 Time 的時間類別,它記錄了一天當中的時間,定義如下:

class Time:
	"""代表一天中的時間
	屬性: hour, minute, second"""

可以試著建立時間類別物件,並給予 hour, minute 和 second 這些屬性值:

time = Time()
time.hour = 11
time.minute = 59
time.second = 30

首先來寫 printTime 函數,讓它顯示時間,顯示的格式為 %.2d ,也就是兩位數風格,沒有的位數需要自動補 0,看起來像是 小時:分鐘:秒鐘

再寫回傳 bool 函數,名為 isAfter,接受兩個時間類別參數,假設是 t1 與 t2,如果 t1 的時間在 t2 之後就回傳 True,否則回傳 False。

在開發 isAfter 可外加挑戰條件,就是不能使用 if 敘述。先試著寫寫看,再繼續往下閱讀。

def printTime(t):
	"""印出時間函數
	參數: t 時間類別""" 
	print('%.2d:%.2d:%.2d' % (t.hour, t.minute, t.second))

def isAfter(t1, t2):
	"""如果 t1 的時間在 t2 之後就回傳 True,否則回傳 False"""
	return (t1.hour, t1.minute, t1.second) > (t2.hour, t2.minute, t2.second)

16-2 純函數

接著撰寫名為 addTime 的函數,它是純函數,功能是將兩個時間相加。同時也利用撰寫這個函數的機會,提供從簡單到複雜的函數版本。但實際撰寫時,必須因應不同的狀況改寫函數,以符合需求。

def addTime(t1, t2):
	sum = Time()
	sum.hour = t1.hour + t2.hour
	sum.minute = t1.minute + t2.minute
	sum.second = t1.second + t2.second
	return sum

在上例函數中,建立了新的 Time 類別,什麼是純函數?當函數沒有改變任何參數值,沒有列印輸出,也沒有提供輸入,只有一個結果的回傳值時,這就是純函數。

因為要測試這個函數功能,於是建立兩個時間物件作為參數給 addTime。

addTime 會告訴我們一場電影將播放到何時,或者一場球賽進行了多久等,以下是使用範例:

>>> start = Time()
>>> start.hour = 9
>>> start.minute = 45
>>> start.second =  0

>>> duration = Time()
>>> duration.hour = 1
>>> duration.minute = 35
>>> duration.second = 0

>>> done = addTime(start, duration)
>>> printTime(done)
10:80:00

這個 10:80:00 並不是我們想要的結果,因為原本的函數只是把給定的時間參數值加起來就回傳,沒有考慮到超過 60 的進位問題。

在日常生活中,我們習慣超過 60 秒進位為分,超過 60 分進位為時,才能符合實際情境,以下是改良過的 addTime 版本:

def addTime(t1, t2):
	sum = Time()
	sum.hour = t1.hour + t2.hour
	sum.minute = t1.minute + t2.minute
	sum.second = t1.second + t2.second

	if sum.second >= 60:
		sum.second -= 60
		sum.minute += 1

	if sum.minute >= 60:
		sum.minute -= 60
		sum.hour += 1

	return sum

乍看之下似乎正確,但事實不然,而且程式碼看來越來越冗長了,需要優化一下,下一節繼續討論。

16-3 修飾函數

有些情況下,修改傳進函數的參數是個好方法,有這種特質的函數稱為修飾函數。

這裡有個 increment 函數草稿,它的功能是增加傳進來的時間物件秒數:

def increment(time, seconds):
	time.second += seconds

	if time.second >= 60:
		time.second -= 60
		time.minute += 1

	if time.minute >= 60:
		time.minute -= 60
		time.hour += 1

原本只是想增加秒數,但必須考慮進位的問題,進位可能是秒進到分,也可能從分進到時,都要進行處理。

曾經寫過的功能或敘述可以重複再利用,這就是為什麼要將功能寫成函數、類別等設計的其中一個原因——重構再利用。

讓我們再思考一下,這個函數真的完全正確了嗎?

假設秒數加起來後是 135 ,遠大於 60 ,那麼進位就不只是 1 而已,需要進 2 ,所以不能只處理一遍。

有個做法是把 if 改寫成 while 迴圈,但效率不是很好。應該撰寫另外一個更好的版本,留在練習一讓大家撰寫,外加三個挑戰條件:

  • 不能使用任何內建函數
  • 不能使用 if 判斷敘述
  • 不能使用任何迴圈

一般說來,可用修飾函數寫法完成的功能,通常也可使用純函數的寫法來完成,甚至有些程式語言只允許寫純函數而已。

通常純函數的效能優於修飾函數,但修飾函數比較方便撰寫與開發,因為它對初學者更直覺。

我們推薦純函數的撰寫風格,除非修飾函數有明顯的優勢,純函數的被利用率也會比較高。

在練習二中,來練習寫純函數版的 increment,就是在函數內建立時間物件,處理完後直接回傳。

16-4 原形與規劃

一開始撰寫草稿或計畫來解決問題時,稱為函數的原形,然後會依據實際狀況使用增量開發,接著可能是在增量開發與除錯的循環交替中,完成函數的撰寫。

隨著程式碼的複雜度增加,除錯會越來越難,因為很難確定是否已經發現所有的錯誤。特別是重構程式碼後,需要一一重新測試所有的狀況,這相當耗時。

這邊提出實際的狀況,還記得上一節的 addTime 吧,當我們覺得程式碼越來越長時,就會出現新的思維。

想把轉換的部分抽離成另外的函數,名為 timeToInt,讓 addTime 負責做累加時間的工作就好,並且使用另一種轉換方式。

大家都知道一小時有 60 分,一分鐘有 60 秒,而一小時有 3600 秒。秒需要進位為分,分進位為時,所以統一將時間換算成最小單位,也就是秒:

def timeToInt(time):
	minutes = time.hour * 60 + time.minute
	seconds = minutes * 60 + time.second
	return seconds

接著撰寫一個把總秒數換算回 ISO格式時間 的函數 intToTime:

def intToTime(seconds):
	time = Time()
	(minutes, time.second) = divmod(seconds, 60)
	(time.hour, time.minute) = divmod(minutes, 60)
	return time

需要再次測試一些數據,以確保上面的函數功能正常,特別是未進位及需進位的情境,都要被測試到才行。

例如 timeToInt(intToTime(x)) == x

這個式子只要相等,就表示這兩個函數沒有問題。

如果測試通過,最後,我們改寫原本的 addTime:

def addTime(t1, t2):
	seconds = timeToInt(t1) + timeToInt(t2)
	return intToTime(seconds)

這個版本看起來簡潔有力且結果正確。在練習三中,練習利用這兩個函數 timeToInt 與 intToTime 改寫 increment 函數吧。

相較於我們習慣的 10 進位轉換, 60 進位的轉換需要多思考一下,但值得這麼做,因為能編寫出更良好的函數,並且在執行除錯時的結果更加可靠。

同時,利用上述思維寫出兩個相減的時間等等函數,對於整個時間類別而言,轉換是經常需要做的事情,因此值得花時間來優化它。

除錯

以時間而言,先不管秒數為毫秒(小數)的問題,時間的換算基本上是一種常識。

分和秒在 0 與 60 之間,但不包括 60,且若時間為正,那這種時間格式是正確的。

這樣的定義或算式是不變式,也就是等式。在所有情況下,此條件為真,都是成立的。

因此會需要編寫一個自行命名的函數,暫時稱為 validTime ,來驗證時間格式是否正確:

def validTime(time):
	if time.hour < 0 or time.minute < 0 or time.second < 0:
		return False
	if time.minute >= 60 or time.second >= 60:
		return False
	return True

因此在執行本章那些時間函數時,應先考慮測試帶進來的時間參數是否符合格式:

def addTime(t1, t2):
	if not validTime(t1) or not validTime(t2):
		raise ValueError('在 addTime 的參數不符合正確的時間格式')
	seconds = timeToInt(t1) + timeToInt(t2)
	return intToTime(seconds)

或是使用 assert 斷言語句,當不變式失敗時,則引發異常:

def addTime(t1, t2):
	assert validTime(t1) and validTime(t2)
	seconds = timeToInt(t1) + timeToInt(t2)
	return intToTime(seconds)

assert 很方便地將正確與錯誤的程式碼分開,也沒有巢狀 if 的問題。

當程式中的 assert 斷言式子為假時,就會中斷整個程式的運作,也能自訂斷言的訊息:

assert validTime(t1) and validTime(t2), '在 addTime 的參數不符合正確的時間格式'
# 當引發 assert 時結果
# AssertionError: 在 addTime 的參數不符合正確的時間格式

當格式不正確時,原本函數的功能應該略過,因為錯誤的格式不可能有正確的結果。

將規則寫下並歸納後,才能轉換出正確的判斷程式碼。有規則就會有邏輯,讓後續的每一步更精準。

當程式寫不好時,回頭檢視每個環節,回到原點,從條列需求開始做起。

動動腦

1.命令列參數

  1. 什麼是命令列參數(引數)?
  2. 函數原形是什麼?
import sys

# 檔名
print(sys.argv[0])
# 第一個參數
#print(sys.argv[1])
print(len(sys.argv))

2.函數原形

無論是 python 的手冊或文件,表示函數的回傳值與參數,會有固定的表示法。

還記得之前讓大家去讀英文的手冊查看函數的第八章習題吧?現在我們舉個例子,帶著大家看懂函數原形,以經常使用在 for 迴圈的 range 函數來說明:

range([start], stop[, step])

最前面的 range 是名稱,小括號 () 表示它是個函數。

函數裡面的參數至少要一個 stop,最多有三個。而用中括號 [] 包起來的,表示可以被省略。

沒有中括號的參數就是必要的參數,所以這樣解讀:

  • start: 起始值,預設為0,參數值可以省略
  • stop: 停止條件,必要參數不可省略
  • step: 計數器的增減值,預設值為1,可以省略

這是一種簡潔有力的表示法,能夠看懂這樣的表示法很重要,在查詢手冊時,針對這個函數,看一行便清楚整個函數的定義,甚至可能不需要看範例,就大概知道該怎麼使用它。

習題

練習 1 檔名: 16-1.py

請見講義16-3

練習 2 檔名: 16-2.py

請見講義16-3

練習 3 檔名: 16-3.py

請見講義16-4

練習 4 檔名: 16-4.py

寫一個函數接受使用者輸入他的生日,再寫一個函數回傳使用者到今天為止的年齡。輸入時需要做錯誤處理,並且設定輸入格式。

條件是需要建立 Date 類別,裡面包含年月日等屬性,函數能夠操作這個類別。

提示:可利用 datetime 模組

練習 5 檔名: 16-5.py

利用本章所學,新增函數,假設每個月為30天,功能是傳進兩個日期類別當作函數參數相加,再回傳結果。

相加時要注意進位問題,可以用一個或多個函數完成這個任務。

提示:可利用 datetime 模組

參考資料

assert 斷言

驗證日期格式

影片

第十六章 類別與函數

最後更新:2021-10-23 09:56:31

From: 111.249.166.140

By: 特種兵