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,就會發生一件奇怪的事情

>>> 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 子句在迴圈完成最後一次迭代後執行,也就是說,如果沒有發生中斷。

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!
...

4.7. match 語句

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

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

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 匹配,則不會執行任何分支。

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

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

你可以將位置引數與某些為其屬性提供順序的內建類(例如資料類)一起使用。你還可以透過在類中設定 __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。(有關文件字串的更多資訊,請參閱 文件字串 部分。)有一些工具使用文件字串自動生成線上或列印文件,或讓使用者以互動方式瀏覽程式碼;在編寫的程式碼中包含文件字串是一個好習慣,所以養成這個習慣。

函式的執行會引入一個新的符號表,用於函式的區域性變數。更準確地說,函式中的所有變數賦值都會將值儲存在區域性符號表中;而變數引用首先查詢區域性符號表,然後在封閉函式的區域性符號表中查詢,然後在全域性符號表中查詢,最後在內建名稱表中查詢。因此,全域性變數和封閉函式的變數不能在函式內直接賦值(除非對於全域性變數,在 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, and 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**kwds 之間存在潛在的衝突,後者將 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. 任意引數列表

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

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 表示式來返回一個函式。另一個用途是將小函式作為引數傳遞。

>>> 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 個空格)後測試空白的等效性。

以下是一個多行文件字串的示例:

>>> 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 字元。

腳註