跳到內容

Think Python/迭代

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

多重賦值

[編輯 | 編輯原始碼]

正如你可能已經發現,對同一個變數進行多次賦值是合法的。新的賦值會使現有變數引用新值(並停止引用舊值)。

bruce = 5
print bruce,
bruce = 7
print bruce

該程式的輸出為5 7,因為第一次bruce被列印時,其值為 5,第二次列印時,其值為 7。第一個print語句末尾的逗號抑制了換行符,因此兩個輸出都出現在同一行上。

以下是多重賦值在狀態圖中的樣子

<IMG SRC="book010.png">

對於多重賦值,區分賦值操作和等式語句尤為重要。由於 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

在更新變數之前,你必須初始化它,通常使用簡單的賦值

將變數加 1 稱為遞增;減 1 稱為遞減while語句

[編輯 | 編輯原始碼]

計算機通常用於自動化重複性任務。重複相同或相似的任務而不會出錯是計算機擅長的事情,而人類則不擅長。

我們已經看到了兩個程式,countdownprint_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,還沒有人能夠證明反駁它!

練習 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 = 
x + a/x
2
 

例如,如果 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.

精確地表示。與其檢查xy如果兩個數字完全相等,使用內建函式abs計算它們之間的差的絕對值或幅度更安全。

    if abs(y-x) &lt; epsilon:
        break

其中epsilon的值為0.0000001這決定了多接近才足夠接近。

練習 2  

將此迴圈封裝到一個名為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)')
&lt;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.
華夏公益教科書