1. 程式人生 > >我要翻譯《Think Python》- 006 第四章 學習案例:介面設計

我要翻譯《Think Python》- 006 第四章 學習案例:介面設計

本文翻自:Allen B. Downey ——《Think Python》
原文連結:http://www.greenteapress.com/thinkpython/html/thinkpython005.html
翻譯:Simba Gu

[自述:感覺從這一章開始算是有點“乾貨”了,很容易激起初學者的興趣,想當年上學的時候就是老師隨便寫的一個迴圈語句讓兩個大於號“>>”沿著螢幕繞圈就像貪吃蛇一樣,吸引了我並且讓我入了寫程式這個“坑”]

第四章

學習案例:介面設計


4.1 TurtleWorld

  為了配合本書,我寫了一個Swampy包,你可以從這裡下載並按照上面的說明安裝到你的系統:

http://thinkpython.com/swampy
  包是一個模組的集合,TurtleWorld就是Swampy裡面的一個模組,它提供了一些引導小海龜在螢幕上畫線的函式集。你只要在系統中安裝了Swampy包就可以匯入TurtleWorld模組:
  

from swampy.TurtleWorld import *


  如果你下載了Swampy包,但是沒有安裝,你可以在Swampy的根目錄除錯程式,或者把該目錄新增到Python可以搜尋的路徑中去,然後再這樣匯入TurtleWorldlike :
  

from TurtleWorld import *


  安裝過程和Python搜尋路徑的設定,取決於你的系統,本書就不作具體描述,如有疑問請參考此連結:http://thinkpython.com/swampy
  建立一個檔案mypolygon.py,輸入下面的程式碼:
  

from swampy.TurtleWorld import *

world = TurtleWorld()
bob = Turtle()
print bob

wait_for_user()

  第一行程式碼表示從swampy包匯入TurtleWorld 模組。接下來一行建立一個TurtleWorld 物件和Turtle物件分別賦值給變數world和bob。列印bob變數你會看到類似這樣的結果:

<TurtleWorld.Turtle instance at 0xb7bfbf4c>

  這表示bob變數引用了TurtleWorld 模組的一個例項 Turtle。從上下文中可以看出,“例項”表示集合的一個成員,這裡的Turtle例項可以是一組或者集合中的一個。
  這裡的 wait_for_user 告訴TurtleWorld 等待使用者下一步操作,儘管在這個案例中除了等使用者關閉視窗之外並無其它。
  TurtleWorld 提供了一些turtle轉向的函式:fd和bk用於前進和後退,lt和rt用於左右轉彎。並且每一個Turtle物件都有一支“筆”,可以落下或提起,如果筆落下,當Turtle物件移動的時候就會留下痕跡。函式pu和pd分別表示“提筆”和“落筆”。
  下面的程式碼可以畫一個直角(程式碼放在建立的bob物件和 wait_for_user 函式之間):  

fd(bob, 100)
lt(bob)
fd(bob, 100)


  第一行程式碼讓bob向前移動100,第二行讓它向左轉彎。當你執行程式的時候,你就可以看到bob向東向北移動並留下了執行軌跡。
  請修改程式碼畫一個正方形。在實現此功能之前請不要繼續本章內容!


4.2 簡單迴圈

  你可能寫了這樣的程式碼(此處省略了建立TurtleWorld 物件和呼叫wait_for_user 函式)

fd(bob, 100)
lt(bob)

fd(bob, 100)
lt(bob)

fd(bob, 100)
lt(bob)

fd(bob, 100)

  我們還可以用for迴圈語句來實現重複的功能。請新增以下程式碼到mypolygon.py指令碼檔案並執行:

for i in range(4):
    print 'Hello!'

  你應該可以看到下面的結果:

Hello!
Hello!
Hello!
Hello!

  這個例子裡面用到了for語句,後面我們還會看到更多。但是這足以讓你重新編寫畫正方形的程式,下面就是for語句實現的程式碼:

for i in range(4):
    fd(bob, 100)
    lt(bob)

  for語句的語法類似函式定義,它有一個以冒號結尾的頭和縮排的正文,正文內可以包含任意數量的語句。
  for語句有時被稱為迴圈,因為執行流程經過正文處理之後又回到迴圈的頂部。在這個案例中,正文內容被執行了4次。
  這裡的程式碼跟之前畫正方形的程式碼有些不同,因為在畫出了正方形之後又進行了一次轉彎,這樣會花費額外的處理時間,但是如果是重複的動作,這樣會簡化程式碼,而且for迴圈的這個版本讓Turtle回到原點之後也恢復了初始的方向。

4.3 練習

  下面是TurtleWorld系列的一些練習,這些本來是為了好玩,但是其中也不乏一些程式設計思想。當你在練習的時候請思考一下重點是什麼。
  本書提供了下面練習的解決方案,你可以先嚐試一下,不要直接抄答案。

    1. 編寫一個名為square的函式,它傳遞一個名為 t 的turtle物件引數,實現用turtle物件畫一個正方形。編寫一個函式呼叫,將bob作為引數傳遞給square,然後再次執行程式。

    2. 在square函式新增另一個名為length的引數。修改函式內容,實現所畫正方形的邊長度為length,然後修改函式呼叫,加入第二個引數,再次執行程式。使用一定長度範圍的值來測試程式。

  3. 預設情況下,lt和rt函式進行90度旋轉,但您可以提供第二個引數,指定角度的數量。例如,lt(bob, 45) 可以讓bob向左旋轉45度。複製一個square函式,把它的名字改成polygon。再新增另一個名為n的引數並修改polygon函式主體,使其繪製一個n邊正多邊形。提示:n邊正多邊形的外角是360/n 度。

  4. 編寫一個名為circle的函式,該函式以turtle物件 t 和半徑 r 為引數,通過呼叫具有適當長度和邊數的多邊形來繪製一個近似圓。用一定範圍的 r 來測試函式。

      • 提示:求出圓的周長,確保 length * n = circumference。
      • 另一個提示:如果你覺得 bob 速度太慢,你可以通過改變bob.delay(移動間隔時間)來加快速度,以秒為單位,例如 bob.delay = 0.01。

  5. 製作一個更通用的circle 函式,新增一個額外的引數angle,用來決定畫一個圓弧的哪個部分。以angle為單位,當angle=360時,circle 函式就會畫一個完整的圓。


4.4 封裝

  上文中的第一個練習要求將畫正方形圖形的程式碼放入函式定義中,然後呼叫函式,並將turtle作為引數傳遞。解決方案如下:

def square(t):
    for i in range(4):
    fd(t, 100)
    lt(t)

square(bob)

  在最內層的語句,fd 和 lt 縮排兩次以表名它們位於for迴圈中,而for迴圈位於函式定義中。函式呼叫行square(bob)與左側空白齊平,因此這是for迴圈和函式定義的結尾。
  在函式內部,t 表示Turtle物件bob,因此 lt(t) 與 lt(bob) 具有相同的效果。那這裡為什麼不直接呼叫引數bob呢?這是因為這裡的 t 可以是任何Turtle物件,而不僅僅是bob,因為你可以建立另一個Turtle物件並將它作為引數傳遞給square函式:

ray = Turtle()
square(ray)


  在函式中包裝一段程式碼稱為封裝。封裝的好處之一是它可以將一個名稱附加到程式碼上,作為一種文件。另一個優點是,如果重用程式碼,呼叫函式兩次比複製和貼上主體更簡單方便!

4.5 泛化

  接下來是給square函式新增一個引數length。實現程式碼如下:

def square(t, length):
    for i in range(4):
        fd(t, length)
        lt(t)

square(bob, 100)

  向函式新增引數稱為泛化,因為它使函式更通用:在以前的版本中,正方形的大小是相同的;在這個版本中,它可以是可變的。
  下一步也是泛化。polygon 函式是畫出任意數量的正多邊形,而不是正方形。實現程式碼如下:

def polygon(t, n, length):
    angle = 360.0 / n
    for i in range(n):
        fd(t, length)
        lt(t, angle)

polygon(bob, 7, 70)

  這段程式碼將繪製一個邊長度為70的7邊形。如果函式有多個數值引數,那將會很容易忘記它們是什麼,或者它們應該處於什麼順序。在引數列表中包含引數的名稱是合法的,有時是很有幫助的:

polygon(bob, n=7, length=70)

  這些被稱為關鍵字引數,因為它們包含引數名作為“關鍵字”(不要與Python關鍵字,如while和def等混淆)。
  這種語法使程式更具可讀性,還提醒了引數和引數是如何工作的:當您呼叫一個函式時,實引數被分配給形參。

4.6 介面設計

  下一步就是寫以半徑 r 為引數的circle函式。這裡有一個簡單的解決方案,就是用polygon 函式畫一個50面的多邊形。

def circle(t, r):
    circumference = 2 * math.pi * r
    n = 50
    length = circumference / n
    polygon(t, n, length)


  第一行計算圓的周長,公式為2π*r。因為我們使用到了pi值,所以需要匯入math模組。按照慣例,通常import語句都位於指令碼的開頭。
  n是畫一個圓需要的近似的線的段數,所以length是每個線段的長度。因此,polygon 函式繪製了一個50邊的多邊形,它近似於一個半徑為 r 的圓。
  這個解決方案有一個限制就是 n 是常數,這意味著對於很大的圓,每一個線段太長,對於小的圓來說又浪費時間畫很小的線段。因此,一個解決方案是把n作為引數來泛化這個函式。這將給使用者(無論誰呼叫circle函式)更多的控制,但介面將會變得有些亂。
  函式的介面是如何使用它:引數是什麼?函式可以做什麼?返回值是多少?如果介面“儘可能簡單,但不簡單”,那麼它就是“精煉”的。(愛因斯坦)
  在本例中,變數 r 屬於介面,因為它指定了要繪製的圓。n 則不是,因為它是函式內部如何渲染圓的區域性變數。
  因此,與其讓介面混亂,還不如根據周長選擇合適的 n 的值:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = int(circumference / 3) + 1
    length = circumference / n
    polygon(t, n, length)


  現在畫線的段數是(大約)circumference/3,所以每個段的長度是(大約)3,每一段線的長度足夠小,這樣畫出來的圓看起來才平滑,只要線的段數大到足夠有效,就可以畫出任何大小的圓。

4.7 重構

  當我在寫circle函式的時候我可以重用polygon函式,應為一個許多邊的多邊形就是一個近似的圓。但是圓弧卻不能重用circle和polygon函式。
  有一個可選的方法就是複製一份polygon函式,再改成 arc 函式。改完之後大致如下:

def arc(t, r, angle):
    arc_length = 2 * math.pi * r * angle / 360
    n = int(arc_length / 3) + 1
    step_length = arc_length / n
    step_angle = float(angle) / n

    for i in range(n):
        fd(t, step_length)
        lt(t, step_angle)


  這個函式的後半部分看起來像polygon函式,但是我們不能在不改變介面的情況下重用polygon函式。我們可以泛化polygon函式以一個角度作為第三個引數,但polygon將不再是一個合適的函式名字! 我們可以用一個更通用的函式名稱polyline:

def polyline(t, n, length, angle):
    for i in range(n):
        fd(t, length)
        lt(t, angle)    


  因此可以把polyline函式重新改寫polygon函式和arc 函式:

def polygon(t, n, length):
    angle = 360.0 / n
    polyline(t, n, length, angle)

def arc(t, r, angle):
    arc_length = 2 * math.pi * r * angle / 360
    n = int(arc_length / 3) + 1
    step_length = arc_length / n
    step_angle = float(angle) / n
    polyline(t, n, step_length, step_angle)

  最後,我們可以用arc函式重寫circle函式:

def circle(t, r):
    arc(t, r, 360)


  這個過程——重新安排程式以改進功能介面並促進程式碼重用可以稱為“重構”。在本例中,我們注意到在arc和polygon函式中有類似的程式碼,因此我們將其分解為polyline函式。
  如果我們提前規劃程式碼,我們可能會首先編寫polyline函式並避免重構,但通常在專案開始的時候,您對程式設計中所需要的介面還不夠了解。只有在開始編寫程式碼之後,您才會更好地理解問題。某種程度上來說,當你開始重構的函式的時候標誌著你已經學會了一些東西了。


4.8 開發方案

  開發計劃是一個編寫程式的過程。我們在本案例研究中使用的過程是“封裝和泛化”。這項工作的步驟如下:
  首先編寫一個沒有函式定義的小程式。
  一旦程式可以正常執行,再把它封裝在一個函式中,並且給函式起個名字。
  通過新增適當的引數來拓展該函式。
  重複步驟1–3,直到你有一個函式的集合。複製並貼上工作程式碼,以避免重複輸入(和重新除錯)。
  通過重構尋找改程序序的機會。例如,如果您在幾個地方有類似的程式碼,考慮將其分解為適當的通用函式。
  這個過程是有一些缺點的——我們在本書的後面會有替代方案——如果你不知道如何將程式劃分為函式,這也不影響你繼續本書的學習。

4.9 文件字串

  docstring是函式開頭的一個字串,用於解釋介面(“doc”是“documentation”的縮寫)。這裡有一個例子:

def polyline(t, n, length, angle):
    """Draws n line segments with the given length and
    angle (in degrees) between them. t is a turtle.
    """ 
    for i in range(n):
        fd(t, length)
        lt(t, angle)


  這個docstring是一個用三引號括起來的字串,也稱為多行字串,因為三元引號允許字串跨越多行。
  它很簡潔,但是它包含了一些函式所需要的重要資訊。它簡明地解釋了函式的作用(沒有詳細介紹它是如何完成的)。它解釋了每個引數對函式行為的影響,以及每個引數應該是什麼型別(如果不是很明顯的話)。
  編寫這種文件是介面設計的一個重要部分。設計良好的介面應該很容易解釋;如果您在解釋您的某個函式時遇到了困難,這意味著這個介面還可以再改進。

4.10 除錯

  介面就像函式和呼叫者之間的契約。呼叫方同意提供某些引數,該函式同意執行某些工作。
  例如,polyline 函式需要四個引數:t 必須是Turtle物件,n是線段的數目,所以n必須是一個整數;length應該是一個正數;angle必須是一個數字,表示角度的意思。
  這些需求被稱為先決條件,因為它們應該在函式開始執行之前準備好。相反,函式末尾的條件是後置條件。後置條件包括函式的預期效果(比如畫線段)和任何副加作用(如移動Turtle或在TurtleWorld中進行其他修改)。
  先決條件是呼叫方的責任。如果呼叫方違反了一個(適當的文件化的)先決條件,並且函式不能正常工作,那問題就在函式呼叫的地方,而不是函式裡面。

4.11 術語表

例項:
  一個集合中的成員。本章中的TurtleWorld是TurtleWorld集合的成員。
迴圈:
  程式中可以重複執行的部分。
封裝:
  將語句序列轉換為函式定義的過程。
概括:
  用適當的通用(如變數或引數)替換不必要的特定物件(如數字)的過程。
關鍵引數:
  包含引數名稱作為“關鍵字”的引數。
介面:
  描述如何使用一個函式,包括引數的名稱和描述以及返回值。
重構:
  修改工作程式的過程,以改進函式介面和程式碼的其他質量。
開發計劃:
  編寫程式的過程。
文件字串:
  在函式定義中顯示的用於記錄函式介面的字串。
先決條件:
  函式啟動前呼叫方應該滿足的需求。
後置條件:
  函式結束前應該滿足的需求。

4.12 練習

練習 1
  從http://thinkpython.com/code/polygon.py下載本章下載本章中的程式碼。
  為polygon、arc 和circle函式編寫適當的文件。
  繪製一個堆疊圖,顯示執行圓時程式的狀態(bob,radius)。您可以手工進行算術或向程式碼中新增列印語句。
  第4.7節中弧的版本不是很精確,因為圓的線性近似總是在真圓之外。因此導致Turtle離正確的目的地還差了幾個單位。我給出的解決方案減小此錯誤影響的方法。請閱讀程式碼,看看它對您是否有意義。如果你畫一個圖表,你可能會看清除它是如何工作的。

圖4.1


練習 2
  編寫一組適當的通用函式,可以繪製如圖4.1所示的圖案。
  解決方案:
  http://thinkpython.com/code/flor.py
  http://thinkpython.com/code/polygon.py

圖4.2


練習 3
  編寫一組適當的通用函式,可以繪製如圖4.2所示的形狀。
  解決方案:http://thinkpython.com/code/pie.py

練習 4
  字母表中的字母可以用一定數量的基本元素構成,如垂直線和水平線以及一些曲線。設計一種字型,它可以用最少的基本元素繪製,然後編寫繪製字母的函式。
  您需要為每個字母編寫一個函式,並命名為draw_a, draw_b, ...等,並將您的函式放入名為letters.py 的檔案。你可以從可以從 http://thinkpython.com/code/typewriter.py 下載一個下載一個“Turtle打字機”來幫助你測試你的程式碼。
  解決方案:
  http://thinkpython.com/code/letters.py
  http://thinkpython.com/code/polygon.py

練習 5
  請閱讀請閱讀 http://en.wikipedia.org/wiki/Spiral 上的相關文章;然後編寫一個繪製阿基米德螺旋(或其他種類的程式)
  解決方案:
  http://thinkpython.com/code/spiral.py.

#英文版權  Allen B. Downey
#翻譯中文版權  Simba Gu
#轉載請註明出處