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' 的縮寫,對於避免過度縮排很有用。ifelifelif … 序列是其他語言中 switchcase 語句的替代品。

如果您要將相同的值與多個常量進行比較,或者檢查特定型別或屬性,您可能還會發現 match 語句很有用。有關更多詳細資訊,請參閱 match 語句

4.2. for 語句

Python 中的 for 語句與您在 C 或 Pascal 中可能習慣的有所不同。Python 的 for 語句不是始終迭代數字的算術級數(如 Pascal 中),也不是讓使用者能夠定義迭代步長和停止條件(如 C 中),而是按它們在序列中出現的順序迭代任何序列(列表或字串)的項。例如(並非雙關語)

>>> # Measure some strings:
>>> words = ['cat', 'window', 'defenestrate']
>>> for w in words:
...     print(w, len(w))
...
cat 3
window 6
defenestrate 12

在迭代同一集合時修改集合的程式碼可能很難正確實現。相反,通常更直接的做法是遍歷集合的副本或建立新集合

# Create a sample collection
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

# Strategy:  Iterate over a copy
for user, status in users.copy().items():
    if status == 'inactive':
        del users[user]

# Strategy:  Create a new collection
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 的序列中各項的合法索引。可以使範圍從另一個數字開始,或指定不同的增量(甚至是負數;有時這被稱為“步長”)

>>> 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(10)
range(0, 10)

在許多方面,range() 返回的物件行為類似於列表,但實際上它不是。它是一個物件,當您迭代它時,它會返回所需序列的連續項,但它實際上不生成列表,從而節省了空間。

我們稱這樣的物件是 可迭代的,也就是說,它適合作為期望從中獲取連續項直到供應耗盡的函式和構造的目標。我們已經看到 for 語句就是這樣一個構造,而接受可迭代物件的函式示例是 sum()

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

稍後我們將看到更多返回可迭代物件並接受可迭代物件作為引數的函式。在 資料結構 一章中,我們將更詳細地討論 list()

4.4. breakcontinue 語句

break 語句會跳出最內層的 forwhile 迴圈

>>> 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 子句

forwhile 迴圈中,break 語句可以與 else 子句配對。如果迴圈在沒有執行 break 的情況下完成,則 else 子句執行。

for 迴圈中,else 子句在迴圈完成其最後一次迭代後執行,即如果沒有發生 break。

while 迴圈中,它在迴圈條件變為假後執行。

在任一型別的迴圈中,如果迴圈被 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:
...         # loop fell through without finding a factor
...         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 子句與 try 語句的 else 子句更相似,而不是與 if 語句的 else 子句相似:try 語句的 else 子句在沒有異常發生時執行,而迴圈的 else 子句在沒有 break 發生時執行。有關 try 語句和異常的更多資訊,請參閱 處理異常

4.6. pass 語句

pass 語句什麼也不做。當語法要求有語句但程式不需要任何操作時,可以使用它。例如

>>> while True:
...     pass  # Busy-wait for keyboard interrupt (Ctrl+C)
...

這通常用於建立最小類

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

另一個可以使用 pass 的地方是,當您正在編寫新程式碼時,它可以用作函式或條件體的佔位符,讓您可以在更抽象的層面上思考。pass 會被默默地忽略

>>> def initlog(*args):
...     pass   # Remember to implement this!
...

對於最後一種情況,許多人使用省略號字面量 ... 而不是 pass。這種用法對 Python 沒有特殊含義,也不是語言定義的一部分(你可以在這裡使用任何常量表達式),但 ... 也被約定俗成地用作佔位符主體。請參閱 省略號物件

4.7. match 語句

一個 match 語句接受一個表示式,並將其值與一個或多個 case 塊給出的連續模式進行比較。這表面上類似於 C、Java 或 JavaScript(以及許多其他語言)中的 switch 語句,但它更類似於 Rust 或 Haskell 等語言中的模式匹配。只有第一個匹配的模式才會被執行,並且它還可以從值中提取元件(序列元素或物件屬性)到變數中。如果沒有 case 匹配,則不執行任何分支。

最簡單的形式是將主題值與一個或多個字面量進行比較

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"

請注意最後一個塊:“變數名”_ 充當萬用字元,永遠不會匹配失敗。

你可以使用 |(“或”)在一個模式中組合多個字面量

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

模式可以像解包賦值一樣,並可用於繫結變數

# point is an (x, y) tuple
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")

仔細研究一下這個!第一個模式有兩個字面量,可以看作是上面所示字面量模式的擴充套件。但是接下來的兩個模式結合了一個字面量和一個變數,並且變數將主題(point)中的值繫結到自身。第四個模式捕獲兩個值,這使得它在概念上類似於解包賦值 (x, y) = point

如果您正在使用類來構造資料,您可以使用類名後跟一個類似於建構函式的引數列表,但能夠將屬性捕獲到變數中

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")

您可以將位置引數與一些內建類一起使用,這些類為其屬性提供了一個順序(例如 dataclasses)。您還可以透過在類中設定 __match_args__ 特殊屬性來定義模式中屬性的特定位置。如果將其設定為 ("x", "y"),則以下模式都是等效的(並且都將 y 屬性繫結到 var 變數)

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

推薦閱讀模式的方式是將其視為賦值左側內容的擴充套件形式,以瞭解哪些變數會被設定為哪些值。只有獨立的名稱(如上文的 var)會被 match 語句賦值。帶點號的名稱(如 foo.bar)、屬性名稱(上文的 x=y=)或類名稱(透過其旁邊的“(...)”識別,如上文的 Point)永遠不會被賦值。

模式可以任意巢狀。例如,如果我們有一個帶 __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 子句,稱為“守衛”。如果守衛為假,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")

此語句的其他幾個關鍵特性

  • 像解包賦值一樣,元組和列表模式具有完全相同的含義,並且實際上匹配任意序列。一個重要的例外是它們不匹配迭代器或字串。

  • 序列模式支援擴充套件解包:[x, y, *rest](x, y, *rest) 的工作方式類似於解包賦值。* 後面的名稱也可以是 _,所以 (x, y, *_) 匹配一個至少包含兩個項的序列,而不繫結其餘項。

  • 對映模式:{"bandwidth": b, "latency": l} 從字典中捕獲 "bandwidth""latency" 的值。與序列模式不同,多餘的鍵會被忽略。還支援像 **rest 這樣的解包。(但 **_ 將是多餘的,因此不允許。)

  • 可以使用 as 關鍵字捕獲子模式

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

    會將輸入的第二個元素捕獲為 p2(只要輸入是兩個點的序列)

  • 大多數字面量透過相等性進行比較,但單例 TrueFalseNone 透過標識進行比較。

  • 模式可以使用命名常量。這些必須是點式名稱,以防止它們被解釋為捕獲變數

    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. 定義函式

我們可以建立一個函式,將斐波那契數列寫入任意邊界

>>> def fib(n):    # write Fibonacci series less than n
...     """Print a Fibonacci series less than n."""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # Now call the function we just defined:
>>> fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

關鍵字 def 引入一個函式定義。它後面必須跟著函式名和帶括號的形式引數列表。構成函式主體的語句從下一行開始,並且必須縮排。

函式體的第一個語句可以是可選的字串字面量;這個字串字面量是函式的文件字串,或稱 docstring。(關於 docstring 的更多資訊可以在 文件字串 一節中找到。)有一些工具使用 docstring 自動生成線上或列印文件,或者讓使用者互動式地瀏覽程式碼;在您編寫的程式碼中包含 docstring 是一種良好的實踐,因此請養成這個習慣。

函式的執行引入了一個新的符號表,用於函式的區域性變數。更準確地說,函式中的所有變數賦值都將值儲存在區域性符號表中;而變數引用首先在區域性符號表中查詢,然後在外圍函式的區域性符號表中查詢,然後在全域性符號表中查詢,最後在內建名稱表中查詢。因此,全域性變數和外圍函式的變數不能在函式內部直接賦值(除非,對於全域性變數,在 global 語句中命名;或者,對於外圍函式的變數,在 nonlocal 語句中命名),儘管它們可以被引用。

函式呼叫時,實際引數(實參)被引入到被呼叫函式的區域性符號表中;因此,引數透過 值傳遞(這裡的 始終是一個物件 引用,而不是物件的值)傳遞。[1] 當一個函式呼叫另一個函式,或者遞迴地呼叫自身時,會為該呼叫建立一個新的區域性符號表。

函式定義將函式名與當前符號表中的函式物件關聯起來。直譯器將該名稱指向的物件識別為使用者定義函式。其他名稱也可以指向同一個函式物件,並且也可以用來訪問該函式

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

來自其他語言的你可能會反對 fib 不是一個函式而是一個過程,因為它不返回任何值。事實上,即使沒有 return 語句的函式也會返回一個值,儘管這個值相當無聊。這個值被稱為 None(它是一個內建名稱)。如果 None 是唯一寫入的值,直譯器通常會抑制它的輸出。如果你真的想看到它,可以使用 print()

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

編寫一個返回斐波那契數列數字列表而不是列印它的函式很簡單

>>> def fib2(n):  # return Fibonacci series up to n
...     """Return a list containing the Fibonacci series up to n."""
...     result = []
...     a, b = 0, 1
...     while a < n:
...         result.append(a)    # see below
...         a, b = b, a+b
...     return result
...
>>> f100 = fib2(100)    # call it
>>> f100                # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

這個例子,像往常一樣,展示了一些新的 Python 特性

  • return 語句從函式返回一個值。return 不帶表示式引數會返回 None。函式執行到末尾也會返回 None

  • 語句 result.append(a) 呼叫列表物件 result 的一個方法。方法是“屬於”一個物件的函式,命名為 obj.methodname,其中 obj 是某個物件(這可以是一個表示式),methodname 是由物件型別定義的方法的名稱。不同的型別定義不同的方法。不同型別的方法可以有相同的名稱而不會引起歧義。(可以使用來定義自己的物件型別和方法,請參閱 類初探)示例中顯示的 append() 方法是為列表物件定義的;它在列表末尾新增一個新元素。在此示例中,它等效於 result = result + [a],但效率更高。

4.9. 更多函式定義

還可以定義具有可變數量引數的函式。有三種形式,可以組合使用。

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('你真的想退出嗎?')

  • 提供一個可選引數:ask_ok('確定要覆蓋檔案嗎?', 2)

  • 甚至提供所有引數:ask_ok('確定要覆蓋檔案嗎?', 2, '來吧,只有是或否!')

本例還介紹了 in 關鍵字。它測試序列是否包含某個值。

預設值在函式定義時的定義作用域中評估,因此

i = 5

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

i = 6
f()

將列印 5

重要警告: 預設值只評估一次。當預設值是可變物件時,例如列表、字典或大多數類的例項,這會產生差異。例如,以下函式在後續呼叫時會累積傳遞給它的引數

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 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump')         # 3 positional arguments
parrot('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

但以下所有呼叫都將無效

parrot()                     # required argument missing
parrot(voltage=5.0, 'dead')  # non-keyword argument after a keyword argument
parrot(110, voltage=220)     # duplicate value for the same argument
parrot(actor='John Cleese')  # unknown keyword 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 形式的最終形式引數時,它會收到一個字典(參見 對映型別 — dict),其中包含除對應於形式引數的關鍵字引數之外的所有關鍵字引數。這可以與 *name 形式的形式引數(在下一小節中描述)結合使用,後者會收到一個包含超出形式引數列表的位置引數的 元組。(*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

其中 /* 是可選的。如果使用,這些符號表示引數透過何種方式傳遞給函式:僅位置引數、位置或關鍵字引數,以及僅關鍵字引數。關鍵字引數也稱為命名引數。

4.9.3.1. 位置或關鍵字引數

如果函式定義中不存在 /*,則引數可以透過位置或關鍵字傳遞給函式。

4.9.3.2. 僅位置引數

更詳細地看,可以將某些引數標記為僅位置引數。如果是僅位置引數,則引數的順序很重要,並且引數不能透過關鍵字傳遞。僅位置引數放置在 /(正斜槓)之前。/ 用於邏輯上將僅位置引數與其他引數分開。如果函式定義中沒有 /,則沒有僅位置引數。

/ 後面的引數可以是位置或關鍵字引數,也可以是僅關鍵字引數。

4.9.3.3. 僅關鍵字引數

要將引數標記為僅關鍵字,表示引數必須透過關鍵字引數傳遞,請在引數列表中的第一個僅關鍵字引數之前放置一個 *

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'

最後,考慮這個函式定義,它可能在位置引數 name 和以 name 為鍵的 **kwds 之間發生衝突

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. 任意引數列表

最後,最不常用的選項是指定函式可以呼叫任意數量的引數。這些引數將被包裝在一個元組中(參見 元組和序列)。在可變數量的引數之前,可以出現零個或多個普通引數。

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

通常,這些可變引數將位於形式引數列表的末尾,因為它們會收集所有傳遞給函式的剩餘輸入引數。*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. 解包引數列表

當引數已經在一個列表或元組中,但需要解包以供需要單獨位置引數的函式呼叫時,會出現相反的情況。例如,內建的 range() 函式期望單獨的startstop引數。如果它們不能單獨獲得,請使用 * 運算子編寫函式呼叫,以從列表或元組中解包引數

>>> list(range(3, 6))            # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))            # call with arguments unpacked from a list
[3, 4, 5]

同樣,字典可以使用 ** 運算子提供關鍵字引數

>>> 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 函式可以在需要函式物件的地方使用。它們在語法上僅限於單個表示式。從語義上講,它們只是普通函式定義的語法糖。像巢狀函式定義一樣,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. 文件字串

以下是關於文件字串內容和格式的一些約定。

第一行應始終是物件用途的簡短、簡潔摘要。為簡潔起見,它不應明確說明物件的名稱或型別,因為這些可透過其他方式獲得(除非名稱恰好是描述函式操作的動詞)。此行應以大寫字母開頭並以句號結尾。

如果文件字串中有更多行,則第二行應為空,以視覺上將摘要與描述的其餘部分分開。接下來的行應為一個或多個段落,描述物件的呼叫約定、其副作用等。

Python 解析器不會從 Python 中的多行字串字面量中剝離縮排,因此處理文件的工具如果需要,必須剝離縮排。這是透過以下約定完成的。字串第一行之後的第一個非空行決定了整個文件字串的縮排量。(我們不能使用第一行,因為它通常緊鄰字串的起始引號,因此其縮排在字串字面量中不明顯。)然後,從字串所有行的開頭剝離與此縮排“等效”的空白。縮排較少的行不應出現,但如果出現,其所有前導空白都應被剝離。空白的等效性應在製表符(通常為 8 個空格)擴充套件後進行測試。

這是一個多行 docstring 的示例

>>> def my_function():
...     """Do nothing, but document it.
...
...     No, really, it doesn't do anything.
...     """
...     pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.

No, really, it doesn't do anything.

4.9.8. 函式註解

函式註解 是關於使用者定義函式所使用的型別的完全可選的元資料資訊(有關更多資訊,請參閱 PEP 3107PEP 484)。

註解 儲存在函式的 __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. 插曲:編碼風格

既然您即將編寫更長、更復雜的 Python 程式碼,現在是討論編碼風格的好時機。大多數語言都可以用不同的風格編寫(或者更簡潔地說,格式化);有些比其他的更具可讀性。讓其他人輕鬆閱讀您的程式碼總是一個好主意,而採用良好的編碼風格對此有巨大幫助。

對於 Python,PEP 8 已成為大多數專案所遵循的風格指南;它提倡一種非常易讀且賞心悅目的編碼風格。每個 Python 開發人員都應該在某個時候閱讀它;這裡為您提取了最重要的幾點

  • 使用 4 個空格縮排,不使用製表符。

    4 個空格是小縮排(允許更大的巢狀深度)和大縮排(更易閱讀)之間的良好折衷。製表符會引起混淆,最好不用。

  • 換行以使它們不超過 79 個字元。

    這有助於使用小顯示器的使用者,並使得在更大的顯示器上可以並排顯示多個程式碼檔案。

  • 使用空行分隔函式和類,以及函式內部較大的程式碼塊。

  • 儘可能將註釋放在單獨的一行。

  • 使用文件字串。

  • 運算子周圍和逗號後使用空格,但括號結構內部不直接使用:a = f(1, 2) + g(3, 4)

  • 一致地命名你的類和函式;約定是類使用 UpperCamelCase,函式和方法使用 lowercase_with_underscores。總是使用 self 作為第一個方法引數的名稱(關於類和方法的更多資訊,請參閱 類初探)。

  • 如果您的程式碼旨在用於國際環境,請勿使用花哨的編碼。Python 的預設編碼 UTF-8,甚至純 ASCII,在任何情況下都能最好地工作。

  • 同樣,如果程式碼有絲毫可能被說不同語言的人閱讀或維護,請勿在識別符號中使用非 ASCII 字元。

腳註