# 第四章 案例學習:交互設計
本章會提供一個案例,用于展示如何卻設計一些共同工作的函數。
本章介紹了小烏龜這個模塊,這允許你用小龜的圖形功能來制作一些圖形。烏龜模塊在大部分的 Python 中都有安裝,不過如果你在線使用 PythnAnywhere,你就無法運行這些烏龜樣例了(至少我寫這本教材的時候還不行)。
(譯者注:都學到第四章了,你還不本地安裝個 Python 也太說不過去了吧。)
如果你已經安裝了 Python 在你的電腦上,你就能運行這些例子了。沒安裝的話呢,這就是安裝的好時機了唄。我已經把相關介紹放到網頁上面了,[點擊訪問](http://tinyurl.com/thinkpython2e)。
本章代碼樣例可以點擊[此鏈接](http://thinkpython2.com/code/polygon.py)來下載了。
## 4.1 烏龜模塊
要檢查你是不是已經安裝了這個烏龜模塊,你要打開 Python 解釋器來輸入如下內容:
```py
>>> import turtle
>>> bob = turtle.Turtle()
```
運行上述例子的時候,應該就能新建一個小窗口,還有個小箭頭象征小烏龜。如果有的話就對了,把窗口關掉吧先。
建立一個叫做 mypolygon.py 的文件,在里面輸入如下內容:
```py
import turtle
bob = turtle.Turtle()
print(bob)
turtle.mainloop()
```
這個小烏龜模塊(記著是小寫的 t)提供了一個叫做 Turtle(注意這里是大寫的,大小寫要去分!)的函數,這個函數會創建一個 Turtle 對象,我們把它賦值給 bob 這個變量。打印一下 bob 就能顯示如下內容:
```Bash
<turtle.Turtle object at 0xb7bfbf4c>
```
這就意味著 bob 已經指向了模塊 turtle 中所定義的 Turtle 類的一個對象。
mainloop 這個函數是告訴窗口等用戶來做些事情,當然本次嘗試的情況下用戶也就是關閉窗口而已了。
一旦你創建了一個 Trutle,你就可以調用一些方法讓他在窗口中移動。方法跟函數有點相似,但語法的使用稍微不太一樣。比如你可以讓小烏龜往前走:
```py
bob.fd(100)
```
fd 這個方法,是 turtle 類這個叫做 bob 的對象所包含的。調用這個方法就像是做出一個請求一樣:你再讓 bob 向前移動。fd 這個方法的參數是像素數距離,所以實際的大小依賴于你顯示器的情況了。
Turtle 對象中還有一些其他方法,比如 bk 是后退,lt 是左轉,rt 是右轉。lt 和 rt 用偏轉角度做參數。
另外,每個 Turtle 都相當于帶著筆,可以落下或者抬起;如果筆落下了,Turtle 移動的時候就會留下軌跡了。抬筆落筆的方法縮寫粉筆嗯是 pu 和 pd。
畫一個直角,就要把下面這些線加到程序里面(當然要先創建一個 bob 并且在此之前運行 mainloop):
```py
bob.fd(100)
bob.lt(90)
bob.fd(100)
```
運行這個程序,你就能看到 bob 先向東再往北,后面就留下了兩根互相垂直的線段了。
現在修改一下程序,去畫一個正方形。這個程序運行不好的話就不要繼續后面的章節!
## 4.2 簡單的重復
你估計會寫出如下的內容:
```py
bob.fd(100)
bob.lt(90)
bob.fd(100)
bob.lt(90)
bob.fd(100)
bob.lt(90)
bob.fd(100)
```
上面這個太麻煩了,咱們可以用一個 for 語句來讓這個過程更簡潔。把下面的代碼添加到 mypolygon.py 中然后運行一下:
```py
for i in range(4):
print('Hello!')
```
你將會看到這樣的輸出:
```Bash
Hello!
Hello!
Hello!
Hello!
```
這就是 for 語句的最簡單的一種應用;以后我們會看到更多。不過當前這種簡單的足夠你來重構一下你的正方形繪制程序了。不達目的不罷休,不要跳過困難哈,一定要編寫出來這個再進行后面的內容。
這就是一個用 for 語句來畫正方形的語句:
```py
for i in range(4):
bob.fd(100)
bob.lt(90)
```
for 語句的語法跟函數定義有點相似。有一個頭部,頭部的結尾要用冒號,然后還有一個縮進的循環體。循環體可以包含任意多的語句。
for 語句也被叫做循環,因為運行流程會重復執行循環體。在本節的例子中,循環進行了四次。
這次的正方形繪制代碼實際上和之前的少有不同了,因為在畫完了最后一個邊之后,多了一次轉向。多出來的這部分需要消耗額外的時間,但簡化了下次我們來循環進行繪制的過程。這個版本的代碼也有一個額外的效果:讓小烏龜回到起點,朝著初始方向。
## 4.3 練習
下面是一系列使用 TurtleWorld 的練習。主要就是比較有意思,不過也有一些訓練的作用。你做這些練習的時候,一定要注意考慮這些訓練的作用。
練習后面是有一些樣例的解決方案的,所以你要做完了再往后看,至少你得試試,不會做了看看答案也行哈。
1.寫一個函數叫做 square(譯者注:就是正方形的意思),有一個名叫 t 的參數,這個 t 是一個 turtle。用這個 turtle 來畫一個正方形。寫一個函數調用,把 bob 作為參數傳遞給 square,然后再運行這個程序。
2.給這個 square 函數再加一個參數,叫做 length(譯者注:長度)。把函數體修改一下,讓長度 length 賦值給各個邊的長度,然后修改一下調用函數的代碼,再提供一個這個對應長度的參數。再次運行一下,用一系列不同的長度值來測試一下你的程序。
3.復制一下 square 這個函數,把名字改成 polygon(譯者注:意思為多邊形)。另外添加一個參數叫做 n,然后修改函數體,讓函數實現畫一個正 n 邊的多邊形。提示:正 n 多邊形的外角為 360/n 度。
4.在寫一個叫做 circle(譯者注:圓)的函數,也用一個 turtle 類的對象 t,以及一個半徑 r,作為參數,畫一個近似的圓,通過調用 polygon 函數來近似實現,用適當的邊長和邊數。用不同的半徑值來測試一下你的函數。
提示:算出圓的周長,確保邊長乘以邊數的值(近似)等于圓周長。
5.在 circle 基礎上做一個叫做 arc 的函數,在 circle 的基礎上添加一個 angle(譯者注:角度)變量,用這個角度值來確定畫多大的一個圓弧。用度做單位,當 angle 等于 360 度的時候,arc 函數就應當畫出一個整團了。
## 4.4 封裝
第一個練習讓你把正方形繪制的代碼定義到一個函數里面,然后調用這個函數,傳入一個 turtle 對象作為參數。下面就是個例子了:
```py
def square(t):
for i in range(4):
t.fd(100)
t.lt(90)
square(bob)
```
在最內部的語句里面,fd 和 lt 縮進了兩次,這個意思是他們是 for 循環的循環體內部成員,而 for 循環本身縮進了一次,說明 for 語句被包含在函數的定義當中。接下來的那行 square(bob),緊靠左側,沒有縮進,這說明 for 循環和函數定義都結束了。
在函數體內部,t 所指代的就是小烏龜 bob,因此讓 t 來左轉九十度的效果完全等同于讓 bob 來左轉九十度。本文中沒有把形式參數的名字設置成 bob,這是為啥呢?是因為用 t 可以指代任意一個小烏龜,不僅僅是 bob,所以你就能再創建另一個小烏龜,把它傳遞給 square 這個函數作為實際參數:
```py
alice = Turtle()
square(alice)
```
用函數的形式把一段代碼包裝起來,叫做封裝。這樣有一個好處,就是給代碼起了個名字,有類似文檔說明的功能,更好理解了。另外一個好處是下次重復使用這段代碼的時候,再次調用函數就可以了,這比復制粘貼函數體可方便多了。
## 4.5 泛化
下一步就是給 square 函數添加一個長度參數了。下面是樣例:
```py
def square(t, length):
for i in range(4):
t.fd(length)
t.lt(90)
square(bob, 100)
```
給函數添加參數,就叫做泛化,因為者可以讓函數的功能更廣泛:在之前的版本中,square 這個函數畫出來的正方形總是一個尺寸的;在這個新版本里面,可以自定義邊長了。
下一步也還是泛化。這次就是不光要畫正方形了,要畫一個多邊形,可以指定邊數的。下面是樣例:
```py
def polygon(t, n, length):
angle = 360 / n
for i in range(n):
t.fd(length)
t.lt(angle)
polygon(bob, 7, 70)
```
這個例子畫了一個每個邊長度都為 70 像素的七邊形。
如果你用 Python2 的話,角度可能因為整除而導致的偏差。簡單的解決方法就是用 360.0 來除以 n 而不是用 360,這就是用浮點數替代了原來的整形,結果就是一個浮點數了。
當一個函數有超過一個數據參數的時候,很容易忘掉這些參數都是什么,或者忘掉他們的順序。為了避免這個情況,可以把形式參數的名字包含在一個實際參數列表中:
```py
polygon(bob, n=7, length=70)
```
這些列表叫做關鍵參數列表,因為他們把形式參數的名字作為關鍵詞包含了進來。(注意區別這里的關鍵詞可不是 Python 語言的關鍵詞哈!這里就是字面意思,很關鍵的詞。)
這種語法結構讓程序更容易被人讀懂。也能提醒實際參數和形式參數的使用過程:調用一個函數的時候,把實際參數的值賦給了形式參數。
## 4.6 接口設計
下一步就是寫 circle 這個函數了,需要半徑 r 作為一個參數。下面是一個簡單的樣例,使用 polygon 函數來畫一個 50 邊形,來接近一個圓:
```py
import math
def circle(t, r):
circumference = 2 * math.pi * r
n = 50
length = circumference / n
polygon(t, n, length)
```
第一行計算了圓的周長,使用 2 乘以圓周率再乘以半徑 r。這個計算用到了圓周率,所以要導入 math 模塊。通常都要把導入語句放到整個腳本的開頭。
n 是我們用來逼近一個圓所用的線段數量,所以 length 就是每一個線段的長度了。polygon 畫一個 50 邊的多邊形,來近似做一個半徑為 r 的圓。
這種方案的一個局限性就是 n 是常數,就意味著對于一些大尺寸的圓,線段數目就太多了,而對小的圓,又浪費了很多小線段。解決的方法就是進一步擴展函數,讓函數把 n 也作為一個參數。這就虧讓用戶(調用 circle 函數的任何人)有更多決定權,可以控制所用的線段數量,當然,接口就不那么簡潔了。
函數的接口就是關于它如何工作的一個概述:都有什么變量?函數實現什么功能?以及返回值是什么?允許調用者隨意操作而不用處理一些無關緊要的細節,這種函數接口就是簡潔的。
在本節的例子中,r 包含于接口內,因為要用它來確定所畫圓的大小。n 就不那么合適了,因為它是用來處理如何具體繪制一個圓的。
與其讓接口復雜冗余,更好的思路是讓 n 根據周長來自適應一個合適的值:
```py
def circle(t, r):
circumference = 2 * math.pi * r
n = int(circumference / 3) + 1
length = circumference / n
polygon(t, n, length)
```
現在線段個數就是周長的三分之一了,因此每段線段的長度近似為 3,這個大小可以讓圓看著不錯,也對任意大小的圓都適用了。
## 4.7 重構
當我寫 circle 這個函數的時候,我能利用多邊形函數 polygon 是因為一個足夠多邊的多邊形和圓很接近。但圓弧就不太適合這個思路了;我們不能用多邊形或者圓來畫一個圓弧。
一個替代的方法就是把 polygon 修改一下,轉換成圓弧。結果大概如下所示:
```py
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 = angle / n
for i in range(n):
t.fd(step_length)
t.lt(step_angle)
```
這個函數的后半段看著和多邊形那個還挺像的,但必須修改一下接口才能重利用多邊形的代碼。我們在多邊形函數上增加 angle(角度)作為第三個參數,但繼續叫多邊形就不太合適了,因為不閉合啊!所以就改名叫它多段線 polyline:
```py
def polyline(t, n, length, angle):
for i in range(n):
t.fd(length)
t.lt(angle)
```
現在就可以用多段線 polyline 來重寫多邊形 polygon 和圓弧 arc:
```py
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 的實現了:
```py
def circle(t, r):
arc(t, r, 360)
```
這個過程中,改進了接口設計,增強了代碼再利用,這就叫做重構。在本節的這個例子中,我們先是注意到圓弧 arc 和多邊形 polygon 有相似的代碼,所以我們把他們都用多段線 polyline 來實現。
如果我們事先進行了計劃,估計就會先寫出多段線函數 polyline,然后就不用重構了,但大家在開始一個項目之前往往不一定了解的那么清楚。一旦開始編碼了,你就逐漸更理解其中的問題了。有時候重構就意味著你已經學到了新的內容了。
## 4.8 開發計劃
開發計劃是寫程序的一系列過程。我們本章所用的就是『封裝-泛化』的模式。這一過程的步驟如下:
1. 開始寫一個特別小的程序,沒有函數定義。
2. 一旦有你的程序能用了,確定一下實現功能的這部分有練習的語句,封裝成函數,并命名一下。
3. 通過逐步給這個函數增加參數的方式來泛化。
4. 重復 1-3 步驟,一直到你有了一系列能工作的函數為止。把函數復制粘貼出來,避免重復輸入或者修改了。
5. 看看是不是有通過重構來改進函數的可能。比如,假設你在一些地方看到了相似的代碼,就可以把這部分代碼做成一個函數。
這個模式有一些缺點,我們后續會看到一些替代的方式,但這個模式是很有用的,尤其對耐餓實現不值得怎么去把程序分成多個函數的情況。
## 4.9 文檔字符串
文檔字符串是指:在函數開頭部位,解釋函數的交互接口的字符串,doc 是文檔 documentation 的縮寫。下面是一個例子:
```py
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):
t.fd(length)
t.lt(angle)
```
一般情況下,所有文檔字符串都是三重引用字符串,也被叫做多行字符串,因為三重的單引號表示允許這個字符串是多行的。
這些文字很簡潔,但都包含了一些關鍵的信息,這些信息對于函數使用者來說至關重要。這些信息簡要解釋了函數的用途(不會說細節,也不會說如何實現)。文檔解釋了每個參數對函數行為的影響,以及各自的類型(一般在不是顯而易見的情況下就給解釋了)。
寫這種文檔,對交互接口的設計來說,是至關重要的。設計良好的交互接口應該很容易解釋明白;如果你的函數有一個特別不好解釋了,估計這個函數的交互設計還存在需要改進的地方。
## 4.10 調試
一個交互接口,就像是函數和調用者的一個中間人。調用者提供特定的參數,函數完成特定的任務。
例如,polyline 這個多段線函數,需要四個實際參數:t 必須是一個 Turtle 小烏龜;n(邊數)必須是一個整形;length(長度)應該是一個正數;angle(角度)必須是一個以度為單位的角度值。
這些要求叫做『前置條件』,因為要在函數開始運行之前就要實現才行。相應的在函數的結尾那里的條件叫『后置條件』。后置條件包含函數的預期效果(如畫線段)和其他作用(如移動海龜或進行其他改動)。
前置條件是準備給函數調用者的。如果調用者違背了(妥當標注的)前置條件,然后函數不能正常工作,這個 bug 就會反饋在函數調用者上,而不是函數本身。
如果前置條件得到了滿足,而后置條件未能滿足,這個 bug 就是函數的了。所以如果你的前后置條件都弄清晰,對調試很有幫助。
## 4.11 Glossary 術語列表
method:
A function that is associated with an object and called using dot notation.
>方法:某個類中一個對象所具有的函數,用點連接來進行調用。
loop:
A part of a program that can run repeatedly.
>循環:程序中重復運行的一部分。
encapsulation:
The process of transforming a sequence of statements into a function definition.
>封裝:把一系列相關的語句整理定義成一個函數的過程。
generalization:
The process of replacing something unnecessarily specific (like a number) with something appropriately general (like a variable or parameter).
>泛化:把一些不必要的內容用更廣泛通用的內容來替換掉的過程,比如把一個數字替換成了一個變量或者參數。
keyword argument:
An argument that includes the name of the parameter as a “keyword”.
>關鍵詞參數:一種特殊的實際參數,把形式參數的名字作為關鍵詞包含在內。
interface:
A description of how to use a function, including the name and descriptions of the arguments and return value.
>交互接口:對如何使用一個函數的描述,包括了函數名,以及對實際參數和返回值的描述。
refactoring:
The process of modifying a working program to improve function interfaces and other qualities of the code.
>重構:對一份能工作的程序進行修改,改進函數交互接口以及提高代碼其他方面質量的過程。
development plan:
A process for writing programs.
>開發計劃:寫程序的過程。
docstring:
A string that appears at the top of a function definition to document the function’s interface.
>文檔字符串:一個在函數定義的頂部的字符串,講解函數的交互接口。
precondition:
A requirement that should be satisfied by the caller before a function starts.
>前置條件:函數開始之前,調用者應當滿足的要求。
postcondition:
A requirement that should be satisfied by the function before it ends.
>后置條件:函數結束之前應該滿足的一些要求。
## 4.12 練習
### 練習 1
點擊下面這個鏈接[下載代碼](http://thinkpython2.com/code/polygon.py)。
1. 畫一個棧圖,表明運行函數 circle(bob,radius)時候程序的狀態。你可以手算一下,或者把輸出語句加到代碼上。
2. 4.7 小節中的那個版本的 arc 函數并不太精確,因為對圓進行線性逼近總會超過真實情況。結果就是小烏龜總會距離正確位置偏離一些像素。我的樣例給出了一種降低這種誤差程度的方法。閱讀一下代碼,看你能不能理解。如果你畫一個圖標,也許就能明白代碼是怎么工作的了。
________________________________________

Figure 4.1: Turtle flowers.
________________________________________
### 練習 2
寫一系列的合適的函數組合,畫出圖 4.1 所示的花圖案。
[樣例]( http://thinkpython2.com/code/flower.py)
[以及此鏈接文件](http://thinkpython2.com/code/polygon.py)
________________________________________

Figure 4.2: Turtle pies.
________________________________________
### 練習 3
寫一系列的合適的函數組合,畫出圖 4.2 所示的形狀。
[樣例](http://thinkpython2.com/code/pie.py)
### 練習 4
字母表當中的字母都可以用一定數量的基本元素來構建,比如豎直或者水平的線條,以及一些曲線。設計一個能用最小數量的基本元素畫出來的字母表,然后寫個函數來畫字母出來。
你應當為沒一個字母寫一個函數,名字就比如 draw_a,draw_b 等等,然后把你的函數放到一個叫做 letters.py 的文件中。你可以從這個[鏈接](http://thinkpython2.com/code/typewriter.py) 下載一個烏龜打字機來幫你檢測一下代碼。
你可以參考這里的[樣例](http://thinkpython2.com/code/letters.py);同時還需要[這些](http://thinkpython2.com/code/polygon.py)。
## 練習 5
去[Wiki 百科](http://en.wikipedia.org/wiki/Spiral)看一下螺旋線的相關內容;然后寫個程序來畫阿基米德曲線(曲線中的一種)。[樣例](http://thinkpython2.com/code/spiral.py)