跳轉到內容

Python 程式設計/類和函式

來自華夏公益教科書,開放的書籍,開放的世界

作為使用者定義型別的另一個示例,我們將定義一個名為時間的類,它記錄一天中的時間。類定義如下

class Time(object):
  """represents the time of day.
    attributes: hour, minute, second"""

我們可以建立一個新的時間物件併為小時、分鐘和秒分配屬性

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

時間物件的狀態圖如下所示


<IMG SRC="book025.png">

編寫一個名為 print_time 的函式,它接收一個 Time 物件並以 hour:minute:second 的格式列印它。

提示:格式序列 %.2d 使用至少兩位數字列印整數,如果需要,包括前導零。

編寫一個名為 is_after 的布林函式,它接收兩個 Time 物件 t1t2,如果 t1 在時間上跟隨 t2,則返回 True,否則返回 False

挑戰:不要使用 if 語句。

純函式

[編輯 | 編輯原始碼]

在接下來的幾節中,我們將編寫兩個新增時間值的函式。它們演示了兩種型別的函式:純函式和修飾符。它們還演示了我會稱為 **原型和修補** 的開發計劃,這是一種透過從簡單的原型開始並逐步處理複雜問題來解決複雜問題的方法。

以下是 add_time 的簡單原型

def add_time(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 物件start包含電影的開始時間,例如 Monty Python and the Holy Grail,以及duration包含電影的執行時間,為 1 小時 35 分鐘。

add_time 計算出電影何時結束。

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

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

>>> done = add_time(start, duration)
>>> print_time(done)
10:80:00

結果,10:80:00可能不是你所希望的。問題是此函式沒有處理秒數或分鐘數加起來超過六十的情況。發生這種情況時,我們必須將額外的秒數“進位”到分鐘列,或將額外的分鐘數“進位”到小時列。

這是一個改進的版本

def add_time(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

儘管此函式是正確的,但它開始變得很大。稍後我們將看到一個更短的替代方案。

修飾符

[編輯 | 編輯原始碼]

有時,函式修改它獲取的引數物件很有用。在這種情況下,呼叫者可以看到更改。以這種方式工作的函式被稱為 **修飾符**。

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

第一行執行基本操作;其餘部分處理我們之前見過的特殊情況。

此函式是否正確?如果引數seconds遠大於六十會發生什麼?

在這種情況下,進位一次是不夠的;我們必須不斷地進行下去,直到time.second小於六十。一個解決方案是用if語句替換while語句。這將使函式正確,但不高效。

練習 3 編寫increment 的一個正確版本,它不包含任何迴圈。

任何可以用修飾符完成的事情也可以用純函式完成。事實上,一些程式語言只允許使用純函式。有一些證據表明,使用純函式的程式比使用修飾符的程式開發速度更快且錯誤更少。但修飾符有時很方便,函式式程式往往效率較低。

總的來說,我建議你在合理的情況下編寫純函式,只有在有明顯的優勢時才使用修飾符。這種方法可能被稱為 **函數語言程式設計風格**。

練習 4  編寫increment 的一個“純”版本,它建立一個新的 Time 物件並返回它,而不是修改引數。

原型設計與規劃

[編輯 | 編輯原始碼]

我正在演示的開發計劃稱為“原型和修補”。對於每個函式,我都編寫了一個執行基本計算的原型,然後對其進行了測試,並一路修補錯誤。

這種方法可能很有效,特別是如果你還沒有深入瞭解問題。但增量校正可能會生成不必要的複雜程式碼(因為它處理了許多特殊情況)並且不可靠(因為很難知道你是否已經找到了所有錯誤)。

另一種方法是 **計劃開發**,其中對問題的深入洞察可以使程式設計變得容易得多。在這種情況下,洞察力在於 Time 物件實際上是一個 60 進位制的三位數(見wikipedia.org/wiki/Sexagesimal)!該second屬性是“個位數列”,該minute屬性是“六十位數列”,以及該hour屬性是“三千六百位數列”。

當我們編寫 add_timeincrement時,我們實際上是在做 60 進位制加法,這就是我們必須從一列進位到下一列的原因。

這種觀察表明了另一種解決整個問題的方法——我們可以將 Time 物件轉換為整數,並利用計算機知道如何進行整數運算這一事實。

以下是一個將 Times 轉換為整數的函式

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

以下是將整數轉換為 Times 的函式(回想一下,divmod將第一個引數除以第二個引數,並返回商和餘數作為元組)。

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

你可能需要思考一下,並進行一些測試,才能說服自己這些函式是正確的。測試它們的一種方法是檢查 time_to_int(int_to_time(x)) == x 是否對許多值的x都成立。這是一個一致性檢查的示例。

一旦你確信它們是正確的,你就可以使用它們來重寫 add_time

def add_time(t1, t2):
  seconds = time_to_int(t1) + time_to_int(t2)
  return int_to_time(seconds)

此版本比原始版本更短,並且更容易驗證。

練習 5  

使用time_to_intint_to_time 重寫 'increment'

在某種程度上,從 60 進位制轉換為 10 進位制然後再轉換回來比處理時間更難。進位制轉換更抽象;我們處理時間值的直覺更好。

但如果我們有將時間視為 60 進位制數的洞察力,並對編寫轉換函式(time_to_intint_to_time)進行投資,那麼我們就可以得到一個更短、更易於閱讀和除錯以及更可靠的程式。

它也更容易在以後新增功能。例如,想象一下減去兩個 Times 來找到它們之間的持續時間。幼稚的方法是使用借位來實現減法。使用轉換函式會更容易並且更有可能正確。

具有諷刺意味的是,有時讓問題變得更難(或更通用)會更容易(因為特殊情況和出錯的機會更少)。

如果分鐘seconds的值介於 0 到 60 之間(包括 0 但不包括 60),並且如果小時為正數,則時間物件是格式良好的。小時分鐘應該是整數,但我們可能允許seconds有小數部分。

這些型別的要求稱為不變式,因為它們應該始終為真。換句話說,如果它們不為真,則說明出現了錯誤。

編寫程式碼來檢查您的不變式可以幫助您檢測錯誤並找到其原因。例如,您可能有一個名為 valid_time 的函式,它接受一個時間物件並返回False如果它違反了不變式

def valid_time(time):
  if time.hours &lt; 0 or time.minutes &lt; 0 or time.seconds &lt; 0:
    return False
  if time.minutes >= 60 or time.seconds >= 60:
    return False
  return True

然後,您可以在每個函式的開頭檢查引數以確保它們有效


def add_time(t1, t2):
  if not valid_time(t1) or not valid_time(t2):
    raise ValueError, 'invalid Time object in add_time'
  seconds = time_to_int(t1) + time_to_int(t2)
  return int_to_time(seconds)

或者您可以使用assert語句,它檢查給定的不變式並在失敗時引發異常


def add_time(t1, t2):
  assert valid_time(t1) and valid_time(t2)
  seconds = time_to_int(t1) + time_to_int(t2)
  return int_to_time(seconds)

assert語句很有用,因為它們區分了處理正常情況的程式碼和檢查錯誤的程式碼。

詞彙表

[編輯 | 編輯原始碼]
原型和補丁
一種開發計劃,涉及編寫程式的粗略草稿、測試以及在發現錯誤時進行修正。
計劃開發
一種開發計劃,涉及對問題的高階洞察力和比增量開發或原型開發更多的計劃。
純函式
一個不修改它作為引數接收的任何物件的函式。大多數純函式是有結果的。
修飾符
一個改變它作為引數接收的一個或多個物件的函式。大多數修飾符是無結果的。
函數語言程式設計風格
一種程式設計風格,其中大多數函式是純函式。
不變式
一個在程式執行期間始終為真的條件。

編寫一個名為 mul_time 的函式,它接受一個時間物件和一個數字,並返回一個新的時間物件,該物件包含原始時間和數字的乘積。 然後使用 mul_time 編寫一個函式,它接受一個表示比賽結束時間的時間物件和一個表示距離的數字,並返回一個表示平均配速(每英里時間)的時間物件。

為一個日期物件編寫一個類定義,該物件具有屬性 day month year 。編寫一個名為 increment_date 的函式,它接受一個日期物件 date 和一個整數 n ,並返回一個新的日期物件,該物件表示 date 之後的第 n 天。提示:“九月有三十天……”挑戰:您的函式是否正確處理閏年?參見 wikipedia.org/wiki/Leap_year

datetime 模組提供 date time ,它們類似於本章中的 Date 和 Time 物件,但它們提供了一套豐富的函式和運算子。閱讀 docs.python.org/lib/datetime-date.html 中的文件。

  • 使用 datetime 模組編寫一個程式,該程式獲取當前日期並列印星期幾。
  • 編寫一個程式,它以生日作為輸入並列印使用者的年齡以及到下一個生日的天數、小時、分鐘和秒數。
華夏公益教科書