# 4. 深入了解流程控制

除了剛才介紹的 `while`，這章節還會介紹一些 Python 的陳述式語法。

## 4.1. `if` 陳述式

或許最常見的陳述式種類就是 `if` 了。舉例來說：

    >>> x = int(input("Please enter an integer: "))
    Please enter an integer: 42
    >>> if x < 0:
    ...     x = 0
    ...     print('Negative changed to zero')
    ... elif x == 0:
    ...     print('Zero')
    ... elif x == 1:
    ...     print('Single')
    ... else:
    ...     print('More')
    ...
    More

在陳述式中，可以沒有或有許多個 `elif` 敘述，且 `else` 敘述並不是必要的。關鍵字 `elif` 是「else if」的縮寫，用來避免過多的縮排。一個 `if` ... `elif` ... `elif` ... 序列可以用來替代其他程式語言中的 `switch` 或 `case` 陳述式。

如果你要將同一個值與多個常數進行比較，或者檢查特定的型別或屬性，你可能會發現 `match` 陳述式也很有用。更多的細節，請參閱 match 陳述式。

## 4.2. `for` 陳述式

在 Python 中的 `for` 陳述式有點不同於在 C 或 Pascal 中的慣用方式。相較於只能疊代 (iterate) 一個等差數列（如 Pascal），或給予使用者定義疊代步驟與終止條件（如 C），Python 的 `for` 陳述式疊代任何序列（list 或者字串）的元素，順序與它們出現在序列中的順序相同。例如（無意雙關）：

    >>> # 測量一些字串：
    >>> words = ['cat', 'window', 'defenestrate']
    >>> for w in words:
    ...     print(w, len(w))
    ...
    cat 3
    window 6
    defenestrate 12

在疊代一個集合的同時修改該集合的內容，很難取得想要的結果。比較直觀的替代方式，是疊代該集合的副本，或建立一個新的集合：

    # 建立一個範例集合
    users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

    # 策略：對副本進行疊代
    for user, status in users.copy().items():
        if status == 'inactive':
            del users[user]

    # 策略：建立一個新集合
    active_users = {}
    for user, status in users.items():
        if status == 'active':
            active_users[user] = status

## 4.3. `range()` 函式

如果你需要疊代一個數列的話，使用內建 `range()` 函式就很方便。它可以生成一等差數列：

    >>> for i in range(5):
    ...     print(i)
    ...
    0
    1
    2
    3
    4

給定的結束值永遠不會出現在生成的序列中；`range(10)` 生成的 10 個數值，即對應存取一個長度為 10 的序列內每一個項目的索引值。也可以讓 range 從其他數值開始計數，或者給定不同的公差（甚至為負；有時稱之為 step）：

    >>> list(range(5, 10))
    [5, 6, 7, 8, 9]

    >>> list(range(0, 10, 3))
    [0, 3, 6, 9]

    >>> list(range(-10, -100, -30))
    [-10, -40, -70]

欲疊代一個序列的索引值，你可以搭配使用 `range()` 和 `len()` 如下：

    >>> a = ['Mary', 'had', 'a', 'little', 'lamb']
    >>> for i in range(len(a)):
    ...     print(i, a[i])
    ...
    0 Mary
    1 had
    2 a
    3 little
    4 lamb

然而，在多數的情況，使用 `enumerate()` 函式將更為方便，詳見迴圈技巧。

如果直接印出一個 range 則會出現奇怪的輸出：

    >>> range(10)
    range(0, 10)

在很多情況下，由 `range()` 回傳的物件表現得像是一個 list（串列）一樣，但實際上它並不是。它是一個在疊代時能夠回傳所要求的序列中所有項目的物件，但它不會真正建出這個序列的 list，以節省空間。

我們稱這樣的物件為 iterable（可疊代物件），意即能作為函式及架構中可以一直取得項目直到取盡的對象。我們已經了解 `for` 陳述式就是如此的架構，另一個使用 iterable 的函式範例是 `sum()`：

    >>> sum(range(4))  # 0 + 1 + 2 + 3
    6

待會我們可以看到更多回傳 iterable 和使用 iterable 為引數的函式。在資料結構章節中，我們會討論更多關於 `list()` 的細節。

## 4.4. `break` 和 `continue` 陳述式

`break` 陳述式，終止包含它的最內部 `for` 或 `while` 迴圈：

    >>> for n in range(2, 10):
    ...     for x in range(2, n):
    ...         if n % x == 0:
    ...             print(f"{n} equals {x} * {n//x}")
    ...             break
    ...
    4 equals 2 * 2
    6 equals 2 * 3
    8 equals 2 * 4
    9 equals 3 * 3

`continue` 陳述式讓所屬的迴圈繼續執行下個疊代：

    >>> for num in range(2, 10):
    ...     if num % 2 == 0:
    ...         print(f"Found an even number {num}")
    ...         continue
    ...     print(f"Found an odd number {num}")
    ...
    Found an even number 2
    Found an odd number 3
    Found an even number 4
    Found an odd number 5
    Found an even number 6
    Found an odd number 7
    Found an even number 8
    Found an odd number 9

## 4.5. 迴圈的 `else` 子句

在 `for` 或 `while` 迴圈中，`break` 陳述句可能與 `else` 子句配對。如果迴圈完成而沒有執行 `break`，`else` 子句會被執行。

在 `for` 迴圈中，`else` 子句會在迴圈完成最終的疊代後執行。

在 `while` 迴圈中，它會在迴圈條件變為 false 後執行。

在任何一種迴圈中，如果迴圈由 `break` 終止，則不會執行 `else` 子句。當然其他提早結束迴圈的方式（例如 `return` 或引發例外）也會跳過 `else` 子句的執行。

下面的 `for` 迴圈對此進行了舉例說明，該迴圈用以搜尋質數：

    >>> for n in range(2, 10):
    ...     for x in range(2, n):
    ...         if n % x == 0:
    ...             print(n, 'equals', x, '*', n//x)
    ...             break
    ...     else:
    ...         # 迴圈結束但沒有找到因數
    ...         print(n, 'is a prime number')
    ...
    2 is a prime number
    3 is a prime number
    4 equals 2 * 2
    5 is a prime number
    6 equals 2 * 3
    7 is a prime number
    8 equals 2 * 4
    9 equals 3 * 3

（沒錯，這是正確的程式碼。請看仔細：`else` 子句屬於 `for` 迴圈，並非 `if` 陳述式。）

理解 else 子句的一個方法是將它想像成與迴圈內的 `if` 配對。當迴圈執行時，它會運行如 if/if/if/else 的序列。`if` 在迴圈內，會遇到多次。如果條件曾經為真，`break` 就會發生。如果條件從未為真，迴圈外的 `else` 子句就會執行。

當 `else` 子句用於迴圈時，相較於搭配 `if` 陳述式使用，它的行為與 `try` 陳述式中的 `else` 子句更為相似：`try` 陳述式的 `else` 子句在沒有發生例外 (exception) 時執行，而迴圈的 `else` 子句在沒有任何 `break` 發生時執行。更多有關 `try` 陳述式和例外的介紹，見處理例外。

## 4.6. `pass` 陳述式

`pass` 陳述式不執行任何動作。它可用在語法上需要一個陳述式但程式不需要執行任何動作的時候。例如：

    >>> while True:
    ...     pass  # 忙碌等待鍵盤中斷 (Ctrl+C)
    ...

這經常用於建立簡單的 class（類別）：

    >>> class MyEmptyClass:
    ...     pass
    ...

`pass` 亦可作為一個函式或條件判斷主體的預留位置，在你撰寫新的程式碼時讓你保持在更抽象的思維層次。`pass` 會直接被忽略：

    >>> def initlog(*args):
    ...     pass   # 記得要實作這個！
    ...

在最後這個例子中，許多人會使用刪節號字面值 `...` 來取代 `pass`。這種用法對 Python 並沒有特殊的意義，且並非語言定義的一部分（你也可以在這裡使用任何常數運算式），但 `...` 也慣例上被用作預留主體 (placeholder body)。見 Ellipsis 物件。

## 4.7. `match` 陳述式

`match` 陳述式會拿取一個運算式，並將其值與多個連續的模式 (pattern) 進行比較，這些模式是以一個或多個 case 區塊來表示。表面上，這類似 C、Java 或 JavaScript（以及許多其他語言）中的 switch 陳述式，但它與 Rust 或 Haskell 等語言中的模式匹配 (pattern matching) 更為相近。只有第一個匹配成功的模式會被執行，而它也可以將成分（序列元素或物件屬性）從值中提取到變數中。如果沒有任何的 case 匹配成功，則不會執行任何的分支。

最簡單的形式，是將一個主題值 (subject value) 與一個或多個字面值 (literal) 進行比較：

    def http_error(status):
        match status:
            case 400:
                return "Bad request"
            case 404:
                return "Not found"
            case 418:
                return "I'm a teapot"
            case _:
                return "Something's wrong with the internet"

請注意最後一段：「變數名稱」`_` 是作為通用字元 (wildcard)的角色，且永遠不會匹配失敗。

你可以使用 `|`（「或」）來將多個字面值組合在單一模式中：

    case 401 | 403 | 404:
        return "Not allowed"

模式可以看起來像是拆解賦值 (unpacking assignment)，且可以用來連結變數：

    # point 是一個 (x, y) 元組
    match point:
        case (0, 0):
            print("Origin")
        case (0, y):
            print(f"Y={y}")
        case (x, 0):
            print(f"X={x}")
        case (x, y):
            print(f"X={x}, Y={y}")
        case _:
            raise ValueError("Not a point")

請仔細研究那個例子！第一個模式有兩個字面值，可以想作是之前所述的字面值模式的延伸。但是接下來的兩個模式結合了一個字面值和一個變數，且該變數繫結 (bind) 了來自主題 (`point`) 的一個值。第四個模式會擷取兩個值，這使得它在概念上類似於拆解賦值 `(x, y) = point`。

如果你要用 class 來結構化你的資料，你可以使用該 class 的名稱加上一個引數列表，類似一個建構式 (constructor)，但它能夠將屬性擷取到變數中：

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y

    def where_is(point):
        match point:
            case Point(x=0, y=0):
                print("Origin")
            case Point(x=0, y=y):
                print(f"Y={y}")
            case Point(x=x, y=0):
                print(f"X={x}")
            case Point():
                print("Somewhere else")
            case _:
                print("Not a point")

你可以將位置參數 (positional parameter) 與一些能夠排序其屬性的內建 class（例如 dataclasses）一起使用。你也可以透過在 class 中設定特殊屬性 `__match_args__`，來定義模式中屬性們的特定位置。如果它被設定為 ("x", "y")，則以下的模式都是等價的（且都會將屬性 `y` 連結到變數 `var`）：

    Point(1, var)
    Point(1, y=var)
    Point(x=1, y=var)
    Point(y=var, x=1)

理解模式的一種推薦方法，是將它們看作是你會放在賦值 (assignment) 左側內容的一種延伸形式，這樣就可以了解哪些變數會被設為何值。只有獨立的名稱（像是上面的 `var`）能被 match 陳述式賦值。點分隔名稱（如 `foo.bar`）、屬性名稱（上面的 `x=` 及 `y=`）或 class 名稱（由它們後面的 "(...)" 被辨識，如上面的 `Point`）則永遠無法被賦值。

模式可以任意地被巢套 (nested)。例如，如果我們有一個由某些點所組成的簡短 list，我們就可以像這樣加入 `__match_args__` 來對它進行匹配：

    class Point:
        __match_args__ = ('x', 'y')
        def __init__(self, x, y):
            self.x = x
            self.y = y

    match points:
        case []:
            print("No points")
        case [Point(0, 0)]:
            print("The origin")
        case [Point(x, y)]:
            print(f"Single point {x}, {y}")
        case [Point(0, y1), Point(0, y2)]:
            print(f"Two on the Y axis at {y1}, {y2}")
        case _:
            print("Something else")

我們可以在模式中加入一個 `if` 子句，稱為「防護 (guard)」。如果該防護為假，則 `match` 會繼續嘗試下一個 case 區塊。請注意，值的擷取會發生在防護的評估之前：

    match point:
        case Point(x, y) if x == y:
            print(f"Y=X at {x}")
        case Point(x, y):
            print(f"Not on the diagonal")

此種陳述式的其他幾個重要特色：

- 與拆解賦值的情況類似，tuple（元組）和 list 模式具有完全相同的意義，而且實際上可以匹配任意的序列。一個重要的例外，是它們不能匹配疊代器 (iterator) 或字串。

- 序列模式 (sequence pattern) 可支援擴充拆解 (extended unpacking)：`[x, y, *rest]` 與 `(x, y, *rest)` 的作用類似於拆解賦值。`*` 後面的名稱也可以是 `_`，所以 `(x, y, *_)` 會匹配一個至少兩項的序列，且不會連結那兩項以外的其餘項。

- 對映模式 (mapping pattern)：`{"bandwidth": b, "latency": l}` 能從一個 dictionary（字典）中擷取 `"bandwidth"` 及 `"latency"` 的值。與序列模式不同，額外的鍵 (key) 會被忽略。一種像是 `**rest` 的拆解方式，也是可被支援的。（但 `**_` 則是多餘的做法，所以它並不被允許。）

- 使用關鍵字 `as` 可以擷取子模式 (subpattern)：

      case (Point(x1, y1), Point(x2, y2) as p2): ...

  將會擷取輸入的第二個元素作為 `p2`（只要該輸入是一個由兩個點所組成的序列）。

- 大部分的字面值是藉由相等性 (equality) 來比較，但是單例物件 (singleton) `True`、`False` 和 `None` 是藉由標識值 (identity) 來比較。

- 模式可以使用附名常數 (named constant)。這些模式必須是點分隔名稱，以免它們被解釋為擷取變數：

      from enum import Enum
      class Color(Enum):
          RED = 'red'
          GREEN = 'green'
          BLUE = 'blue'

      color = Color(input("Enter your choice of 'red', 'blue' or 'green': "))

      match color:
          case Color.RED:
              print("I see red!")
          case Color.GREEN:
              print("Grass is green")
          case Color.BLUE:
              print("I'm feeling the blues :(")

關於更詳細的解釋和其他範例，你可以閱讀 PEP 636，它是以教學的格式編寫而成。

## 4.8. 定義函式 (function)

我們可以建立一個函式來產生費式數列到任何一個上界：

    >>> def fib(n):    # 寫出小於 n 的費氏數列
    ...     """印出小於 n 的費氏數列。"""
    ...     a, b = 0, 1
    ...     while a < n:
    ...         print(a, end=' ')
    ...         a, b = b, a+b
    ...     print()
    ...
    >>> # 現在呼叫我們剛才定義的函式：
    >>> fib(2000)
    0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

關鍵字 `def` 介紹一個函式的定義。它之後必須連著該函式的名稱和置於括號之中的一串參數。自下一行起，所有縮排的陳述式成為該函式的主體。

一個函式的第一個陳述式可以是一個字串文本；該字串文本被視為該函式的說明文件字串，即 docstring。（關於 docstring 的細節請參見說明文件字串 (Documentation Strings)段落。）有些工具可以使用 docstring 來自動產生線上或可列印的文件，或讓使用者能以互動的方式在原始碼中瀏覽文件。在原始碼中加入 docstring 是個好慣例，應該養成這樣的習慣。

函式執行時會建立一個新的符號表 (symbol table) 來儲存該函式內的區域變數 (local variable)。更精確地說，所有在函式內的變數賦值都會把該值儲存在一個區域符號表。然而，在引用一個變數時，會先從區域符號表開始搜尋，其次為外層函式的區域符號表，其次為全域符號表 (global symbol table)，最後為所有內建的名稱。因此，在函式中，全域變數及外層函式變數雖然可以被引用，但無法被直接賦值（除非全域變數是在 `global` 陳述式中被定義，或外層函式變數在 `nonlocal` 陳述式中被定義）。

在一個函式被呼叫的時候，實際傳入的參數（引數）會被加入至該函式的區域符號表。因此，引數傳入的方式為傳值呼叫 (call by value)（這裡傳遞的值永遠是一個物件的參照 (reference)，而不是該物件的值）。 [1] 當一個函式呼叫別的函式或遞迴呼叫它自己時，在被呼叫的函式中會建立一個新的區域符號表。

函式定義時，會把該函式名稱加入至目前的符號表。函式名稱的值帶有一個型別，並被直譯器辨識為使用者自定函式 (user-defined function)。該值可以被指定給別的變數名，使該變數名也可以被當作函式使用。這是常見的重新命名方式：

    >>> fib
    <function fib at 10042ed0>
    >>> f = fib
    >>> f(100)
    0 1 1 2 3 5 8 13 21 34 55 89

如果你是來自別的語言，你可能不同意 `fib` 是個函式，而是個程序 (procedure)，因為它並沒有回傳值。實際上，即使一個函式缺少一個 `return` 陳述式，它亦有一個固定的回傳值。這個值稱為 `None`（它是一個內建名稱）。在直譯器中單獨使用 `None` 時，通常不會被顯示。你可以使用 `print()` 來看到它：

    >>> fib(0)
    >>> print(fib(0))
    None

如果要寫一個函式回傳費式數列的 list 而不是直接印出它，這也很容易：

    >>> def fib2(n):  # 回傳到 n 為止的費氏數列
    ...     """回傳包含到 n 為止的費氏數列的串列。"""
    ...     result = []
    ...     a, b = 0, 1
    ...     while a < n:
    ...         result.append(a)    # 見下方
    ...         a, b = b, a+b
    ...     return result
    ...
    >>> f100 = fib2(100)    # 呼叫它
    >>> f100                # 寫出結果
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

這個例子一樣示範了一些新的 Python 特性：

- `return` 陳述式會讓一個函式回傳一個值。單獨使用 `return` 不外加一個運算式作為引數時會回傳 `None`。一個函式執行到結束也會回傳 `None`。

- `result.append(a)` 陳述式呼叫了一個 list 物件 `result` 的 method（方法）。method 為「屬於」一個物件的函式，命名規則為 `obj.methodname`，其中 `obj` 為某個物件（亦可為一運算式），而 `methodname` 為該 method 的名稱，並由該物件的型別所定義。不同的型別定義不同的 method。不同型別的 method 可以擁有一樣的名稱而不會讓 Python 混淆。（你可以使用 class（類別）定義自己的物件型別和 method，見 Class（類別））範例中的 `append()` method 定義在 list 物件中；它會在該 list 的末端加入一個新的元素。這個例子等同於 `result = result + [a]`，但更有效率。

## 4.9. 深入了解函式定義

定義函式時使用的引數 (argument) 數量是可變的。總共有三種可以組合使用的形式。

### 4.9.1. 預設引數值

為一個或多個引數指定預設值是很有用的方式。函式建立後，可以用比定義時更少的引數呼叫該函式。例如：

    def ask_ok(prompt, retries=4, reminder='Please try again!'):
        while True:
            reply = input(prompt)
            if reply in {'y', 'ye', 'yes'}:
                return True
            if reply in {'n', 'no', 'nop', 'nope'}:
                return False
            retries = retries - 1
            if retries < 0:
                raise ValueError('invalid user response')
            print(reminder)

該函式可以用以下幾種方式被呼叫：

- 只給必要引數：`ask_ok('Do you really want to quit?')`

- 給予一個選擇性引數：`ask_ok('OK to overwrite the file?', 2)`

- 給予所有引數：`ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')`

此例也使用了關鍵字 `in`，用於測試序列中是否包含某個特定值。

預設值是在函式定義當下，於定義時的作用域中求值，所以：

    i = 5

    def f(arg=i):
        print(arg)

    i = 6
    f()

將會輸出 `5`。

重要警告：預設值只求值一次。當預設值為可變物件，例如 list、dictionary（字典）或許多類別實例時，會產生不同的結果。例如，以下函式於後續呼叫時會累積曾經傳遞的引數：

    def f(a, L=[]):
        L.append(a)
        return L

    print(f(1))
    print(f(2))
    print(f(3))

將會輸出：

    [1]
    [1, 2]
    [1, 2, 3]

如果不想在後續呼叫之間共用預設值，應以如下方式編寫函式：

    def f(a, L=None):
        if L is None:
            L = []
        L.append(a)
        return L

### 4.9.2. 關鍵字引數

函式也可以使用關鍵字引數，以 `kwarg=value` 的形式呼叫。舉例來說，以下函式：

    def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
        print("-- This parrot wouldn't", action, end=' ')
        print("if you put", voltage, "volts through it.")
        print("-- Lovely plumage, the", type)
        print("-- It's", state, "!")

接受一個必要引數 (`voltage`) 和三個選擇性引數 (`state`，`action`，和 `type`)。該函式可用下列任一方式呼叫：

    parrot(1000)                                          # 1 個位置引數
    parrot(voltage=1000)                                  # 1 個關鍵字引數
    parrot(voltage=1000000, action='VOOOOOM')             # 2 個關鍵字引數
    parrot(action='VOOOOOM', voltage=1000000)             # 2 個關鍵字引數
    parrot('a million', 'bereft of life', 'jump')         # 3 個位置引數
    parrot('a thousand', state='pushing up the daisies')  # 1 個位置引數、1 個關鍵字引數

但以下呼叫方式都無效：

    parrot()                     # 缺少必要引數
    parrot(voltage=5.0, 'dead')  # 關鍵字引數後面的非關鍵字引數
    parrot(110, voltage=220)     # 同一個引數有重複值
    parrot(actor='John Cleese')  # 未知的關鍵字引數

函式呼叫時，關鍵字引數 (keyword argument) 必須在位置引數 (positional argument) 後面。所有傳遞的關鍵字引數都必須匹配一個可被函式接受的引數（`actor` 不是 `parrot` 函式的有效引數），而關鍵字引數的順序並不重要。此規則也包括必要引數，（`parrot(voltage=1000)` 也有效）。一個引數不可多次被賦值，下面就是一個因此限制而無效的例子：

    >>> def function(a):
    ...     pass
    ...
    >>> function(0, a=0)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: function() got multiple values for argument 'a'

當最後一個參數為 `**name` 形式時，它接收一個 dictionary（字典，詳見 Mapping types --- dict, frozendict），該字典包含所有可對應形式參數以外的關鍵字引數。`**name` 可以與 `*name` 參數（下一小節介紹）組合使用，`*name` 接收一個 tuple，該 tuple 包含一般參數以外的位置引數（`*name` 必須出現在 `**name` 前面）。例如，若我們定義這樣的函式：

    def cheeseshop(kind, *arguments, **keywords):
        print("-- Do you have any", kind, "?")
        print("-- I'm sorry, we're all out of", kind)
        for arg in arguments:
            print(arg)
        print("-" * 40)
        for kw in keywords:
            print(kw, ":", keywords[kw])

它可以被如此呼叫：

    cheeseshop("Limburger", "It's very runny, sir.",
               "It's really very, VERY runny, sir.",
               shopkeeper="Michael Palin",
               client="John Cleese",
               sketch="Cheese Shop Sketch")

輸出結果如下：

    -- Do you have any Limburger ?
    -- I'm sorry, we're all out of Limburger
    It's very runny, sir.
    It's really very, VERY runny, sir.
    ----------------------------------------
    shopkeeper : Michael Palin
    client : John Cleese
    sketch : Cheese Shop Sketch

注意，關鍵字引數的輸出順序與呼叫函式時被提供的順序必定一致。

### 4.9.3. 特殊參數

在預設情況，引數能以位置或明確地以關鍵字傳遞给 Python 函式。為了程式的可讀性及效能，限制引數的傳遞方式是合理的，如此，開發者只需查看函式定義，即可確定各項目是按位置，按位置或關鍵字，還是按關鍵字傳遞。

函式定義可能如以下樣式：

    def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
          -----------    ----------     ----------
            |             |                  |
            |        Positional or keyword   |
            |                                - Keyword only
             -- Positional only

`/` 和 `*` 是選擇性的。這些符號若被使用，是表明引數被傳遞給函式的參數種類：僅限位置、位置或關鍵字、僅限關鍵字。關鍵字參數也稱為附名參數 (named parameters)。

#### 4.9.3.1. 位置或關鍵字引數 (Positional-or-Keyword Arguments)

若函式定義中未使用 `/` 和 `*` 時，引數可以按位置或關鍵字傳遞給函式。

#### 4.9.3.2. 僅限位置參數 (Positional-Only Parameters)

此處再詳述一些細節，特定參數可以標記為僅限位置。若參數為僅限位置時，它們的順序很重要，且這些參數不能用關鍵字傳遞。僅限位置參數必須放在 `/`（斜線）之前。`/` 用於在邏輯上分開僅限位置參數與其餘參數。如果函式定義中沒有 `/`，則表示沒有任何僅限位置參數。

`/` 後面的參數可以是位置或關鍵字或僅限關鍵字參數。

#### 4.9.3.3. 僅限關鍵字引數 (Keyword-Only Arguments)

要把參數標記為僅限關鍵字，表明參數必須以關鍵字引數傳遞，必須在引數列表中第一個僅限關鍵字參數前放上 `*`。

#### 4.9.3.4. 函式範例

請看以下的函式定義範例，注意 `/` 和 `*` 記號：

    >>> def standard_arg(arg):
    ...     print(arg)
    ...
    >>> def pos_only_arg(arg, /):
    ...     print(arg)
    ...
    >>> def kwd_only_arg(*, arg):
    ...     print(arg)
    ...
    >>> def combined_example(pos_only, /, standard, *, kwd_only):
    ...     print(pos_only, standard, kwd_only)

第一個函式定義 `standard_arg` 是我們最熟悉的形式，對呼叫方式沒有任何限制，可以按位置或關鍵字傳遞引數：

    >>> standard_arg(2)
    2

    >>> standard_arg(arg=2)
    2

第二個函式 `pos_only_arg` 的函式定義中有 `/`，因此僅限使用位置參數：

    >>> pos_only_arg(1)
    1

    >>> pos_only_arg(arg=1)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'

第三個函式 `kwd_only_arg` 的函式定義透過 `*` 表明僅限關鍵字引數：

    >>> kwd_only_arg(3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given

    >>> kwd_only_arg(arg=3)
    3

最後一個函式在同一個函式定義中，使用了全部三種呼叫方式：

    >>> combined_example(1, 2, 3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: combined_example() takes 2 positional arguments but 3 were given

    >>> combined_example(1, 2, kwd_only=3)
    1 2 3

    >>> combined_example(1, standard=2, kwd_only=3)
    1 2 3

    >>> combined_example(pos_only=1, standard=2, kwd_only=3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: combined_example() got some positional-only arguments passed as keyword arguments: 'pos_only'

最後，請看這個函式定義，如果 `**kwds` 內有 `name` 這個鍵，可能與位置引數 `name` 產生潛在衝突：

    def foo(name, **kwds):
        return 'name' in kwds

呼叫該函式不可能回傳 `True`，因為關鍵字 `'name'` 永遠是連結在第一個參數。例如：

    >>> foo(1, **{'name': 2})
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: foo() got multiple values for argument 'name'
    >>>

使用 `/`（僅限位置引數）後，就可以了。函式定義會允許 `name` 當作位置引數，而 `'name'` 也可以當作關鍵字引數中的鍵：

    >>> def foo(name, /, **kwds):
    ...     return 'name' in kwds
    ...
    >>> foo(1, **{'name': 2})
    True

換句話說，僅限位置參數的名稱可以在 `**kwds` 中使用，而不產生歧義。

#### 4.9.3.5. 回顧

此用例決定哪些參數可以用於函式定義：

    def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):

說明：

- 如果不想讓使用者使用參數名稱，請使用僅限位置。當參數名稱沒有實際意義時，若你想控制引數在函式呼叫的排列順序，或同時使用位置參數和任意關鍵字時，這種方式很有用。

- 當參數名稱有意義，且明確的名稱可讓函式定義更易理解，或是你不希望使用者依賴引數被傳遞時的位置時，請使用僅限關鍵字。

- 對於應用程式介面 (API)，使用僅限位置，以防止未來參數名稱被修改時造成 API 的中斷性變更。

### 4.9.4. 任意引數列表 (Arbitrary Argument Lists)

最後，有個較不常用的選項，是規定函式被呼叫時，可以使用任意數量的引數。這些引數會被包裝進一個 tuple 中（詳見 Tuples 和序列 (Sequences)）。在可變數量的引數之前，可能有零個或多個普通引數：

    def write_multiple_items(file, separator, *args):
        file.write(separator.join(args))

通常，這些 variadic（可變的）引數會出現在參數列表的最末端，這樣它們就可以把所有傳遞給函式的剩餘輸入引數都撈起來。出現在 `*args` 參數後面的任何參數必須是「僅限關鍵字」引數，意即它們只能作為關鍵字引數，而不能用作位置引數。

    >>> def concat(*args, sep="/"):
    ...     return sep.join(args)
    ...
    >>> concat("earth", "mars", "venus")
    'earth/mars/venus'
    >>> concat("earth", "mars", "venus", sep=".")
    'earth.mars.venus'

### 4.9.5. 拆解引數列表（Unpacking Argument Lists）

當引數們已經存在一個 list 或 tuple 裡，但為了滿足一個需要個別位置引數的函式呼叫，而去拆解它們時，情況就剛好相反。例如，內建的 `range()` 函式要求分開的 start 和 stop 引數。如果這些引數不是分開的，則要在呼叫函式時，用 `*` 運算子把引數們從 list 或 tuple 中拆解出來：

    >>> list(range(3, 6))            # 使用分離引數的一般呼叫
    [3, 4, 5]
    >>> args = [3, 6]
    >>> list(range(*args))            # 以從串列解包而得的引數呼叫
    [3, 4, 5]

同樣地，dictionary（字典）可以用 `**` 運算子傳遞關鍵字引數：

    >>> def parrot(voltage, state='a stiff', action='voom'):
    ...     print("-- This parrot wouldn't", action, end=' ')
    ...     print("if you put", voltage, "volts through it.", end=' ')
    ...     print("E's", state, "!")
    ...
    >>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
    >>> parrot(**d)
    -- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

### 4.9.6. Lambda 運算式

`lambda` 關鍵字用於建立小巧的匿名函式。`lambda a, b: a+b` 函式回傳兩個引數的和。Lambda 函式可用於任何需要函式物件的地方。在語法上，它們被限定只能是單一運算式。在語義上，它就是一個普通函式定義的語法糖 (syntactic sugar)。與巢狀函式定義一樣，lambda 函式可以從包含它的作用域中引用變數：

    >>> def make_incrementor(n):
    ...     return lambda x: x + n
    ...
    >>> f = make_incrementor(42)
    >>> f(0)
    42
    >>> f(1)
    43

上面的例子用 lambda 運算式回傳了一個函式。另外的用法是傳遞一個小函式當作引數。舉例來說，`list.sort()` 接受一個鍵排序函式 key，這個函式可以是 lambda 函式：

    >>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
    >>> pairs.sort(key=lambda pair: pair[1])
    >>> pairs
    [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

### 4.9.7. 說明文件字串 (Documentation Strings)

以下是關於說明文件字串內容和格式的慣例。

第一行都是一段關於此物件目的之簡短摘要。為保持簡潔，不應在這裡明確地陳述物件的名稱或型別，因為有其他方法可以達到相同目的（除非該名稱剛好是一個描述函式運算的動詞）。這一行應以大寫字母開頭，以句號結尾。

文件字串為多行時，第二行應為空白行，在視覺上將摘要與其餘描述分開。後面幾行可包含一或多個段落，描述此物件的呼叫慣例、副作用等。

當多行字串字面值作為模組、類別或函式的說明字串時，Python 剖析器會去除其縮排。

下面是多行說明字串的一個範例：

    >>> def my_function():
    ...     """不做任何事，但有文件說明。
    ...
    ...     它真的什麼都不做：
    ...     """
    ...     pass
    ...
    >>> print(my_function.__doc__)
    不做任何事，但有文件說明。

    它真的什麼都不做：

        >>> my_function()
        >>>

### 4.9.8. 函式註釋 (Function Annotations)

函式註釋是選擇性的元資料（metadata）資訊，描述使用者定義函式所使用的型別（更多資訊詳見 PEP 3107 和 PEP 484）。

註釋以 dictionary（字典）的形式存放在函式的 `__annotations__` 屬性中，且不會影響函式的任何其他部分。參數註釋的定義方式是在參數名稱後加一個冒號，冒號後面跟著一個對註釋求值的運算式。回傳註釋的定義方式是在參數列表和 `def` 陳述式結尾的冒號中間，用一個 `->` 文字接著一個運算式。以下範例註釋了一個必要引數、一個選擇性引數，以及回傳值：

    >>> def f(ham: str, eggs: str = 'eggs') -> str:
    ...     print("Annotations:", f.__annotations__)
    ...     print("Arguments:", ham, eggs)
    ...     return ham + ' and ' + eggs
    ...
    >>> f('spam')
    Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
    Arguments: spam eggs
    'spam and eggs'

## 4.10. 間奏曲：程式碼風格 (Coding Style)

現在你即將要寫更長、更複雜的 Python 程式，是時候討論一下編碼樣式了。大多數語言都能以不同的樣式被書寫（或更精確地說，被格式化），而有些樣式比其他的更具可讀性。能讓其他人輕鬆閱讀你的程式碼永遠是一個好主意，而使用優良的編碼樣式對此有極大的幫助。

對於 Python，大多數的專案都遵循 PEP 8 的樣式指南；它推行的編碼樣式相當可讀且賞心悅目。每個 Python 開發者都應該花點時間研讀；這裡是該指南的核心重點：

- 用 4 個空格縮排，不要用 tab 鍵。

  4 個空格是小縮排（容許更大的巢套深度）和大縮排（較易閱讀）之間的折衷方案。Tab 鍵會造成混亂，最好別用。

- 換行，使一行不超過 79 個字元。

  換行能讓使用小顯示器的使用者方便閱讀，也可以在較大顯示器上並排陳列多個程式碼檔案。

- 用空行分隔函式和 class（類別），及函式內較大塊的程式碼。

- 如果可以，把註解放在單獨一行。

- 使用說明字串。

- 運算子前後、逗號後要加空格，但不要直接放在括號內側：`a = f(1, 2) + g(3, 4)`。

- Class 和函式的命名樣式要一致；按慣例，命名 class 用 `UpperCamelCase`（駝峰式大小寫），命名函式與 method 用 `lowercase_with_underscores`（小寫加底線）。永遠用 `self` 作為 method 第一個引數的名稱（關於 class 和 method，詳見 初見 class）。

- 若程式碼是為了用於國際環境時，不要用花俏的編碼。Python 預設的 UTF-8 或甚至普通的 ASCII，就可以勝任各種情況。

- 同樣地，若不同語言使用者閱讀或維護程式碼的可能性微乎其微，就不要在命名時使用非 ASCII 字元。

註解

[ 1 ]

實際上，傳址呼叫 (call by object reference) 的說法可能較為貼切。因為，若傳遞的是一個可變物件時，呼叫者將看得見被呼叫者對物件做出的任何改變（例如被插入 list 內的新項目）。