Think Python/迭代
正如你可能已經發現,對同一個變數進行多次賦值是合法的。新的賦值會使現有變數引用新值(並停止引用舊值)。
bruce = 5
print bruce,
bruce = 7
print bruce
該程式的輸出為5 7,因為第一次bruce被列印時,其值為 5,第二次列印時,其值為 7。第一個print語句末尾的逗號抑制了換行符,因此兩個輸出都出現在同一行上。
以下是多重賦值在狀態圖中的樣子
對於多重賦值,區分賦值操作和等式語句尤為重要。由於 Python 使用等號 (=) 進行賦值,因此很容易將像a = b這樣的語句解釋為等式語句。它不是!
首先,等式是一個對稱關係,而賦值不是。例如,在數學中,如果 a = 7,那麼 7 = a。但在 Python 中,語句a = 7是合法的,而7 = a則不是。
此外,在數學中,等式語句始終為真或假。如果 a = b,那麼 a 將始終等於 b。在 Python 中,賦值語句可以使兩個變數相等,但它們不必保持這種狀態
a = 5
b = a # a and b are now equal
a = 3 # a and b are no longer equal
第三行更改了a的值,但沒有更改b的值,因此它們不再相等。
雖然多重賦值通常很有幫助,但應謹慎使用。如果變數的值頻繁更改,它會使程式碼難以閱讀和除錯。
多重賦值最常見的形式之一是更新,其中變數的新值取決於舊值。
x = x+1
這意味著“獲取x的當前值,加 1,然後用新值更新x。”
如果你嘗試更新一個不存在的變數,你會收到一個錯誤,因為 Python 在為x:
>>> x = x+1
NameError: name 'x' is not defined
賦值之前先評估右側。
>>> x = 0
>>> x = x+1
在更新變數之前,你必須初始化它,通常使用簡單的賦值
計算機通常用於自動化重複性任務。重複相同或相似的任務而不會出錯是計算機擅長的事情,而人類則不擅長。
我們已經看到了兩個程式,countdown和 print_n,它們使用遞迴來執行重複,也稱為迭代。由於迭代非常常見,因此 Python 提供了幾個語言特性來簡化操作。其中之一是我們在第 4.2 節中看到的for語句。我們稍後會回到它。
另一個是while語句。以下是用countdown語句編寫的while語句
def countdown(n):
while n > 0:
print n
n = n-1
print 'Blastoff!'
版本。while你幾乎可以像讀英語一樣閱讀語句。它的意思是,“當n語句。它的意思是,“當大於 0 時,顯示語句。它的意思是,“當的值,然後將的值減 1。當你到達 0 時,顯示文字”
更正式地說,以下是while語句
- 語句的執行流程:評估條件,得到True或.
- Falsewhile如果條件為假,則退出
- 語句,並繼續執行下一條語句。
如果條件為真,則執行主體,然後返回步驟 1。
這種型別的流程稱為迴圈,因為第三步會迴圈回到頂部。
在countdown的情況下,我們可以證明迴圈會終止,因為我們知道語句。它的意思是,“當的值是有限的,並且我們可以看到語句。它的意思是,“當的值在每次迴圈中都變小,因此最終我們必須得到 0。在其他情況下,要判斷是否會終止並不容易
def sequence(n):
while n != 1:
print n,
if n%2 == 0: # n is even
n = n/2
else: # n is odd
n = n*3+1
該迴圈的條件是n != 1,因此迴圈會繼續,直到語句。它的意思是,“當等於1,這使得條件為假。
每次迴圈時,程式都會輸出語句。它的意思是,“當的值,然後檢查它是偶數還是奇數。如果它是偶數,語句。它的意思是,“當除以 2。如果它是奇數,語句。它的意思是,“當的值將被替換為n*3+1。例如,如果傳遞給sequence的引數是 3,則生成的序列為 3, 10, 5, 16, 8, 4, 2, 1。
由於語句。它的意思是,“當有時增加有時減少,因此沒有明顯的證據證明語句。它的意思是,“當最終會達到 1,或者程式會終止。對於語句。它的意思是,“當的某些特定值,我們可以證明終止。例如,如果起始值為 2 的冪,那麼語句。它的意思是,“當的值在每次迴圈中都將為偶數,直到它達到 1。前面的示例以這樣一個序列結束,從 16 開始。
難題在於我們是否可以證明該程式對於所有正值語句。它的意思是,“當都將終止。到目前為止1,還沒有人能夠證明或反駁它!
用迭代而不是遞迴重寫第 '5.8' 節中的函式 print_n。
有時你不知道什麼時候結束迴圈,直到你執行到迴圈主體的一半。在這種情況下,你可以使用break語句跳出迴圈。
例如,假設你想要從使用者那裡獲取輸入,直到他們輸入done。你可以編寫
while True:
line = raw_input('> ')
if line == 'done':
break
print line
print 'Done!'
迴圈條件是評估條件,得到,它始終為真,因此迴圈會一直執行,直到遇到 break 語句。
每次迴圈時,它都會用一個尖括號提示使用者。如果使用者輸入done,則break語句將退出迴圈。否則,程式會回顯使用者輸入的任何內容,然後返回迴圈頂部。以下是一個示例執行
> not done
not done
> done
Done!
這種編寫while迴圈的方法很常見,因為你可以在迴圈中的任何地方檢查條件(而不僅僅是在頂部),並且你可以用肯定的方式表達停止條件(“當發生這種情況時停止”),而不是否定方式(“一直持續到發生這種情況”)。
迴圈經常用於計算數值結果的程式,這些程式從一個近似答案開始,然後迭代地對其進行改進。
例如,計算平方根的一種方法是牛頓法。假設你想要知道 a 的平方根。如果你從幾乎任何估計值 x 開始,你可以使用以下公式計算一個更好的估計值
| y = |
|
例如,如果 a 為 4,x 為 3
>>> a = 4.0
>>> x = 3.0
>>> y = (x + a/x) / 2
>>> print y
2.16666666667
這更接近正確答案 (√4 = 2)。如果我們用新的估計值重複這個過程,它會更接近
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00641025641
再經過幾次更新後,估計值幾乎完全正確
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00001024003
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00000000003
一般來說,我們事先不知道需要多少步才能得到正確答案,但我們知道什麼時候得到答案,因為估計值停止變化
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.0
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.0
當y == x時,我們可以停止。以下是一個迴圈,它從一個初始估計值x開始,並對其進行改進,直到它停止變化
while True:
print x
y = (x + a/x) / 2
if y == x:
break
x = y
對於大多數a的值,這都可以正常工作,但一般來說,測試float的相等性是危險的。浮點數只是近似正確的:大多數有理數,如 1/3,以及無理數,如 √2,不能用float.
精確地表示。與其檢查x和y如果兩個數字完全相等,使用內建函式abs計算它們之間的差的絕對值或幅度更安全。
if abs(y-x) < epsilon:
break
其中epsilon的值為0.0000001這決定了多接近才足夠接近。
將此迴圈封裝到一個名為square_root的函式中,該函式以 'a' 為引數,選擇一個合理的 'x' 值,並返回 'a' 平方根的估計值。
牛頓法是演算法的一個例子:它是一個用於解決一類問題(在本例中是計算平方根)的機械過程。
定義演算法並不容易。從非演算法開始可能會有所幫助。當你學習乘以一位數時,你可能記住了乘法表。實際上,你記住了 100 個特定的解決方案。這種知識不是演算法。
但是,如果你很“懶惰”,你可能透過學習一些技巧來作弊。例如,要找到n和 9 的乘積,你可以將n−1 寫作第一位,將 10−n 寫作第二位。這個技巧是將任何一位數乘以 9 的通用解決方案。這就是演算法!
同樣地,你所學的帶進位加法、帶借位減法和長除法技巧都是演算法。演算法的一個特點是它們不需要任何智慧來執行。它們是機械過程,其中每一步都遵循最後一步的簡單規則。
在我看來,人類在學校花費大量時間學習執行演算法,這真是令人尷尬,因為這些演算法實際上不需要任何智慧。
另一方面,設計算法的過程很有趣,具有智力挑戰性,並且是我們所稱的程式設計的核心部分。
人們自然而然地做的一些事情,沒有任何困難或意識思考,是最難用演算法表達的。理解自然語言就是一個很好的例子。我們都會做,但到目前為止,還沒有人能夠解釋我們是如何做到的,至少不是以演算法的形式。
當你開始編寫更大的程式時,你可能會發現自己花費更多時間除錯。更多的程式碼意味著更多的出錯機會,也意味著更多的地方隱藏錯誤。
減少除錯時間的一種方法是“二分除錯”。例如,如果你的程式中有 100 行程式碼,而你逐行檢查它們,則需要 100 步。
相反,嘗試將問題分成兩半。檢視程式的中間部分或附近部分,以檢查中間值。新增一個print語句(或其他具有可驗證效果的內容)並執行程式。
如果中間點檢查不正確,則問題必須在程式的前半部分。如果正確,則問題在後半部分。
每次執行這樣的檢查時,你都會將要搜尋的程式碼行數減半。在六步之後(這遠小於 100 步),至少在理論上,你會減少到一兩行程式碼。
在實踐中,並不總是清楚“程式的中間部分”是什麼,也不總是可以檢查它。計算行數並找到確切的中間點沒有意義。相反,思考程式中可能存在錯誤的位置以及易於放置檢查的位置。然後選擇一個你認為錯誤在檢查之前或之後的機率大致相同的位置。
- 多重賦值
- 在程式執行期間對同一個變數進行多次賦值。
- 更新
- 變數的新值取決於舊值的賦值。
- 初始化
- 對將要更新的變數賦予初始值的賦值。
- 遞增
- 增加變數值的更新(通常增加 1)。
- 遞減
- 減少變數值的更新。
- 迭代
- 使用遞迴函式呼叫或迴圈重複執行一組語句。
- 無限迴圈
- 迴圈中的終止條件永遠不會滿足。
為了測試本章中的平方根演算法,你可以將其與 'math.sqrt' 進行比較。編寫一個名為test_square_root的函式,該函式列印如下表:
''1.0 1.0 1.0 0.0
2.0 1.41421356237 1.41421356237 2.22044604925e-16
3.0 1.73205080757 1.73205080757 0.0
4.0 2.0 2.0 0.0
5.0 2.2360679775 2.2360679775 0.0
6.0 2.44948974278 2.44948974278 0.0
7.0 2.64575131106 2.64575131106 0.0
8.0 2.82842712475 2.82842712475 4.4408920985e-16
9.0 3.0 3.0 0.0
''
第一列是一個數字 'a';第二列是使用練習 '7.2' 中的函式計算的 'a' 的平方根;第三列是 'math.sqrt' 計算的平方根;第四列是兩個估計值之間差的絕對值。
內建函式 'eval' 接受一個字串並使用 Python 直譯器對其進行求值。例如
''>>> eval('1 + 2 * 3')
7
>>> import math
>>> eval('math.sqrt(5)')
2.2360679774997898
>>> eval('type(math.pi)')
<type 'float'>
''
編寫一個名為eval_loop的函式,該函式以互動方式提示使用者,獲取結果輸入並使用 'eval' 對其進行求值,並列印結果。
它應該繼續直到使用者輸入done,然後返回它最後求值的表示式的值。
天才數學家斯里尼瓦薩·拉馬努金髮現了一個無窮級數2,可用於生成 的數值近似值:
編寫一個名為estimate_pi的函式,該函式使用此公式來計算並返回 'π' 的估計值。它應該使用 'while' 迴圈來計算求和的項,直到最後一項小於 '1e-15'(這是 Python 表示 10−15 的符號)。你可以透過將其與 'math.pi' 進行比較來檢查結果。
你可以在 'thinkpython.com/code/pi.py' 上檢視我的解決方案。
- 1
- 參見wikipedia.org/wiki/Collatz_conjecture.
- 2
- 參見wikipedia.org/wiki/Pi.