Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.5 控制

來源:1.5 Control

譯者:飛龍

協議:CC BY-NC-SA 4.0

我們現在可以定義的函數能力有限,因為我們還不知道一種方法來進行測試,並且根據測試結果來執行不同的操作。控制語句可以讓我們完成這件事。它們不像嚴格的求值子表達式那樣從左向右編寫,並且可以從它們控制解釋器下一步做什麼當中得到它們的名稱。這可能基於表達式的值。

1.5.1 語句

目前為止,我們已經初步思考了如何求出表達式。然而,我們已經看到了三種語句:賦值、defreturn語句。這些 Python 代碼並不是表達式,雖然它們中的一部分是表達式。

要強調的是,語句的值是不相干的(或不存在的),我們使用執行而不是求值來描述語句。 每個語句都描述了對解釋器狀態的一些改變,執行語句會應用這些改變。像我們之前看到的return和賦值語句那樣,語句的執行涉及到求解所包含的子表達式。

表達式也可以作為語句執行,其中它們會被求值,但是它們的值會捨棄。執行純函數沒有什麼副作用,但是執行非純函數會產生效果作為函數調用的結果。

考慮下面這個例子:

>>> def square(x):
        mul(x, x) # Watch out! This call doesn't return a value.

這是有效的 Python 代碼,但是並不是想表達的意思。函數體由表達式組成。表達式本身是個有效的語句,但是語句的效果是,mul函數被調用了,然後結果被捨棄了。如果你希望對錶達式的結果做一些事情,你需要這樣做:使用賦值語句來儲存它,或者使用return語句將它返回:

>>> def square(x):
        return mul(x, x)

有時編寫一個函數體是表達式的函數是有意義的,例如調用類似print的非純函數:

>>> def print_square(x):
        print(square(x))

在最高層級上,Python 解釋器的工作就是執行由語句組成的程序。但是,許多有意思的計算工作來源於求解表達式。語句管理程序中不同表達式之間的關係,以及它們的結果會怎麼樣。

1.5.2 複合語句

通常,Python 的代碼是語句的序列。一條簡單的語句是一行不以分號結束的代碼。複合語句之所以這麼命名,因為它是其它(簡單或複合)語句的複合。複合語句一般佔據多行,並且以一行以冒號結尾的頭部開始,它標識了語句的類型。同時,一個頭部和一組縮進的代碼叫做子句(或從句)。複合語句由一個或多個子句組成。

<header>:
    <statement>
    <statement>
    ...
<separating header>:
    <statement>
    <statement>
    ...
...

我們可以這樣理解我們已經見到的語句:

  • 表達式、返回語句和賦值語句都是簡單語句。
  • def語句是複合語句。def頭部之後的組定義了函數體。

為每種頭部特化的求值規則指導了組內的語句什麼時候以及是否會被執行。我們說頭部控制語句組。例如,在def語句的例子中,我們看到返回表達式並不會立即求值,而是儲存起來用於以後的使用,當所定義的函數最終調用時就會求值。

我們現在也能理解多行的程序了。

  • 執行語句序列需要執行第一條語句。如果這個語句不是重定向控制,之後執行語句序列的剩餘部分,如果存在的話。

這個定義揭示出遞歸定義“序列”的基本結構:一個序列可以劃分為它的第一個元素和其餘元素。語句序列的“剩餘”部分也是一個語句序列。所以我們可以遞歸應用這個執行規則。這個序列作為遞歸數據結構的看法會在隨後的章節中再次出現。

這一規則的重要結果就是語句順序執行,但是隨後的語句可能永遠不會執行到,因為有重定向控制。

**實踐指南:**在縮進代碼組時,所有行必須以相同數量以及相同方式縮進(空格而不是Tab)。任何縮進的變動都會導致錯誤。

1.5.3 定義函數 II:局部賦值

一開始我們說,用戶定義函數的函數體只由帶有一個返回表達式的一個返回語句組成。實際上,函數可以定義為操作的序列,不僅僅是一條表達式。Python 複合語句的結構自然讓我們將函數體的概念擴展為多個語句。

無論用戶定義的函數何時被調用,定義中的子句序列在局部環境內執行。return語句會重定向控制:無論什麼時候執行return語句,函數調用的流程都會中止,返回表達式的值會作為被調用函數的返回值。

於是,賦值語句現在可以出現在函數體中。例如,這個函數以第一個數的百分數形式,返回兩個數量的絕對值,並使用了兩步運算:

>>> def percent_difference(x, y):
        difference = abs(x-y)
        return 100 * difference / x
>>> percent_difference(40, 50)
25.0

賦值語句的效果是在當前環境的第一個幀上,將名字綁定到值上。於是,函數體內的賦值語句不會影響全局幀。函數只能操作局部作用域的現象是創建模塊化程序的關鍵,其中純函數只通過它們接受和返回的值與外界交互。

當然,percent_difference函數也可以寫成一個表達式,就像下面這樣,但是返回表達式會更加複雜:

>>> def percent_difference(x, y):
        return 100 * abs(x-y) / x

目前為止,局部賦值並不會增加函數定義的表現力。當它和控制語句組合時,才會這樣。此外,局部賦值也可以將名稱賦為間接量,在理清複雜表達式的含義時起到關鍵作用。

**新的環境特性:**局部賦值。

1.5.4 條件語句

Python 擁有內建的絕對值函數:

>>> abs(-2)
2

我們希望自己能夠實現這個函數,但是我們當前不能直接定義函數來執行測試並做出選擇。我們希望表達出,如果x是正的,abs(x)返回x,如果x是 0,abx(x)返回 0,否則abs(x)返回-x。Python 中,我們可以使用條件語句來表達這種選擇。

>>> def absolute_value(x):
        """Compute abs(x)."""
        if x > 0:
            return x
        elif x == 0:
            return 0
        else:
            return -x
            
>>> absolute_value(-2) == abs(-2)
True

absolute_value的實現展示了一些重要的事情:

**條件語句。**Python 中的條件語句包含一系列的頭部和語句組:一個必要的if子句,可選的elif子句序列,和最後可選的else子句:

if <expression>:
    <suite>
elif <expression>:
    <suite>
else:
    <suite>

當執行條件語句時,每個子句都按順序處理:

  1. 求出頭部中的表達式。
  2. 如果它為真,執行語句組。之後,跳過條件語句中隨後的所有子句。

如果能到達else子句(僅當所有ifelif表達式值為假時),它的語句組才會被執行。

**布爾上下文。**上面過程的執行提到了“假值”和“真值”。條件塊頭部語句中的表達式也叫作布爾上下文:它們值的真假對控制流很重要,但在另一方面,它們的值永遠不會被賦值或返回。Python 包含了多種假值,包括 0、None和布爾值False。所有其他數值都是真值。在第二章中,我們就會看到每個 Python 中的原始數據類型都是真值或假值。

**布爾值。**Python 有兩種布爾值,叫做TrueFalse。布爾值表示了邏輯表達式中的真值。內建的比較運算符,><>=<===!=,返回這些值。

>>> 4 < 2
False
>>> 5 >= 5
True

第二個例子讀作“5 大於等於 5”,對應operator模塊中的函數ge

>>> 0 == -0
True

最後的例子讀作“0 等於 -0”,對應operator模塊的eq函數。要注意 Python 區分賦值(=)和相等測試(==)。許多語言中都有這個慣例。

**布爾運算符。**Python 也內建了三個基本的邏輯運算符:

>>> True and False
False
>>> True or False
True
>>> not False
True

邏輯表達式擁有對應的求值過程。這些過程揭示了邏輯表達式的真值有時可以不執行全部子表達式而確定,這個特性叫做短路。

為了求出表達式<left> and <right>

  1. 求出子表達式<left>
  2. 如果結果v是假值,那麼表達式求值為v
  3. 否則表達式的值為子表達式<right>

為了求出表達式<left> or <right>

  1. 求出子表達式<left>
  2. 如果結果v是真值,那麼表達式求值為v
  3. 否則表達式的值為子表達式<right>

為了求出表達式not <exp>

  1. 求出<exp>,如果值是True那麼返回值是假值,如果為False則反之。

這些值、規則和運算符向我們提供了一種組合測試結果的方式。執行測試以及返回布爾值的函數通常以is開頭,並不帶下劃線(例如isfiniteisdigitisinstance等等)。

1.5.5 迭代

除了選擇要執行的語句,控制語句還用於表達重複操作。如果我們編寫的每一行代碼都只執行一次,程序會變得非常沒有生產力。只有通過語句的重複執行,我們才可以釋放計算機的潛力,使我們更加強大。我們已經看到了重複的一種形式:一個函數可以多次調用,雖然它只定義一次。迭代控制結構是另一種將相同語句執行多次的機制。

考慮斐波那契數列,其中每個數值都是前兩個的和:

0, 1, 1, 2, 3, 5, 8, 13, 21, ...

每個值都通過重複使用“前兩個值的和”的規則構造。為了構造第 n 個值,我們需要跟蹤我們創建了多少個值(k),以及第 k 個值(curr)和它的上一個值(pred),像這樣:

>>> def fib(n):
        """Compute the nth Fibonacci number, for n >= 2."""
        pred, curr = 0, 1   # Fibonacci numbers
        k = 2               # Position of curr in the sequence
        while k < n:
            pred, curr = curr, pred + curr  # Re-bind pred and curr
            k = k + 1                       # Re-bind k
        return curr
>>> fib(8)
13

要記住逗號在賦值語句中分隔了多個名稱和值。這一行:

pred, curr = curr, pred + curr

具有將curr的值重新綁定到名稱pred上,以及將pred + curr的值重新綁定到curr上的效果。所有=右邊的表達式會在綁定發生之前求出來。

while子句包含一個頭部表達式,之後是語句組:

while <expression>:
    <suite>

為了執行while子句:

  1. 求出頭部表達式。
  2. 如果它為真,執行語句組,之後返回到步驟 1。

在步驟 2 中,整個while子句的語句組在頭部表達式再次求值之前被執行。

為了防止while子句的語句組無限執行,它應該總是在每次通過時修改環境的狀態。

不終止的while語句叫做無限循環。按下<Control>-C可以強制讓 Python 停止循環。

1.5.6 實踐指南:測試

函數的測試是驗證函數的行為是否符合預期的操作。我們的函數現在已經足夠複雜了,我們需要開始測試我們的實現。

測試是系統化執行這個驗證的機制。測試通常寫為另一個函數,這個函數包含一個或多個被測函數的樣例調用。返回值之後會和預期結果進行比對。不像大多數通用的函數,測試涉及到挑選特殊的參數值,並使用它來驗證調用。測試也可作為文檔:它們展示瞭如何調用函數,以及什麼參數值是合理的。

要注意我們也將“測試”這個詞用於ifwhile語句的頭部中作為一種技術術語。當我們將“測試”這個詞用作表達式,或者用作一種驗證機制時,它應該在語境中十分明顯。

**斷言。**程序員使用assert語句來驗證預期,例如測試函數的輸出。assert語句在布爾上下文中只有一個表達式,後面是帶引號的一行文本(單引號或雙引號都可以,但是要一致)如果表達式求值為假,它就會顯示。

>>> assert fib(8) == 13, 'The 8th Fibonacci number should be 13'

當被斷言的表達式求值為真時,斷言語句的執行沒有任何效果。當它是假時,asset會造成執行中斷。

fib編寫的test函數測試了幾個參數,包含n的極限值:

>>> def fib_test():
        assert fib(2) == 1, 'The 2nd Fibonacci number should be 1'
        assert fib(3) == 1, 'The 3nd Fibonacci number should be 1'
        assert fib(50) == 7778742049, 'Error at the 50th Fibonacci number'

在文件中而不是直接在解釋器中編寫 Python 時,測試可以寫在同一個文件,或者後綴為_test.py的相鄰文件中。

**Doctest。**Python 提供了一個便利的方法,將簡單的測試直接寫到函數的文檔字符串內。文檔字符串的第一行應該包含單行的函數描述,後面是一個空行。參數和行為的詳細描述可以跟隨在後面。此外,文檔字符串可以包含調用該函數的簡單交互式會話:

>>> def sum_naturals(n):
        """Return the sum of the first n natural numbers

        >>> sum_naturals(10)
        55
        >>> sum_naturals(100)
        5050
        """
        total, k = 0, 1
        while k <= n:
          total, k = total + k, k + 1
        return total

之後,可以使用 doctest 模塊來驗證交互。下面的globals函數返回全局變量的表示,解釋器需要它來求解表達式。

>>> from doctest import run_docstring_examples
>>> run_docstring_examples(sum_naturals, globals())

在文件中編寫 Python 時,可以通過以下面的命令行選項啟動 Python 來運行一個文檔中的所有 doctest。

python3 -m doctest <python_source_file>

高效測試的關鍵是在實現新的函數之後(甚至是之前)立即編寫(以及執行)測試。只調用一個函數的測試叫做單元測試。詳盡的單元測試是良好程序設計的標誌。