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 內的新項目）。
