4. 其他流程控制工具?

除了上一章介紹的 while 語(yǔ)句,Python 還支持其他語(yǔ)言中常見(jiàn)的流程控制語(yǔ)句,只是稍有不同。

4.1. if 語(yǔ)句?

最讓人耳熟能詳?shù)膽?yīng)該是 if 語(yǔ)句。例如:

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

if 語(yǔ)句包含零個(gè)或多個(gè) elif 子句及可選的 else 子句。關(guān)鍵字 'elif' 是 'else if' 的縮寫(xiě),適用于避免過(guò)多的縮進(jìn)。if ... elif ... elif ... 序列可以當(dāng)作其他語(yǔ)言中 switchcase 語(yǔ)句的替代品。

如果要把一個(gè)值與多個(gè)常量進(jìn)行比較,或者檢查特定類型或?qū)傩裕?code class="xref std std-keyword docutils literal notranslate">match 語(yǔ)句更實(shí)用。詳見(jiàn) match 語(yǔ)句

4.2. for 語(yǔ)句?

Python 的 for 語(yǔ)句與 C 或 Pascal 中的不同。Python 的 for 語(yǔ)句不迭代算術(shù)遞增數(shù)值(如 Pascal),或是給予用戶定義迭代步驟和暫停條件的能力(如 C),而是迭代列表或字符串等任意序列,元素的迭代順序與在序列中出現(xiàn)的順序一致。 例如:

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

遍歷集合時(shí)修改集合的內(nèi)容,會(huì)很容易生成錯(cuò)誤的結(jié)果。因此不能直接進(jìn)行循環(huán),而是應(yīng)遍歷該集合的副本或創(chuàng)建新的集合:

# 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() 函數(shù)?

內(nèi)置函數(shù) range() 常用于遍歷數(shù)字序列,該函數(shù)可以生成算術(shù)級(jí)數(shù):

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

生成的序列不包含給定的終止數(shù)值;range(10) 生成 10 個(gè)值,這是一個(gè)長(zhǎng)度為 10 的序列,其中的元素索引都是合法的。range 可以不從 0 開(kāi)始,還可以按指定幅度遞增(遞增幅度稱為 '步進(jìn)',支持負(fù)數(shù)):

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

不過(guò),大多數(shù)情況下,enumerate() 函數(shù)更便捷,詳見(jiàn) 循環(huán)的技巧 。

如果只輸出 range,會(huì)出現(xiàn)意想不到的結(jié)果:

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

range() 返回對(duì)象的操作和列表很像,但其實(shí)這兩種對(duì)象不是一回事。迭代時(shí),該對(duì)象基于所需序列返回連續(xù)項(xiàng),并沒(méi)有生成真正的列表,從而節(jié)省了空間。

這種對(duì)象稱為可迭代對(duì)象 iterable,函數(shù)或程序結(jié)構(gòu)可通過(guò)該對(duì)象獲取連續(xù)項(xiàng),直到所有元素全部迭代完畢。for 語(yǔ)句就是這樣的架構(gòu),sum() 是一種把可迭代對(duì)象作為參數(shù)的函數(shù):

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

下文將介紹更多返回可迭代對(duì)象或把可迭代對(duì)象當(dāng)作參數(shù)的函數(shù)。 在 數(shù)據(jù)結(jié)構(gòu) 這一章節(jié)中,我們將討論有關(guān) list() 的更多細(xì)節(jié)。

4.4. 循環(huán)中的 break、continue 語(yǔ)句及 else 子句?

break 語(yǔ)句和 C 中的類似,用于跳出最近的 forwhile 循環(huán)。

循環(huán)語(yǔ)句支持 else 子句;for 循環(huán)中,可迭代對(duì)象中的元素全部循環(huán)完畢,或 while 循環(huán)的條件為假時(shí),執(zhí)行該子句;break 語(yǔ)句終止循環(huán)時(shí),不執(zhí)行該子句。 請(qǐng)看下面這個(gè)查找素?cái)?shù)的循環(huán)示例:

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

(沒(méi)錯(cuò),這段代碼就是這么寫(xiě)。仔細(xì)看:else 子句屬于 for 循環(huán),不屬于 if 語(yǔ)句。)

if 語(yǔ)句相比,循環(huán)的 else 子句更像 tryelse 子句: tryelse 子句在未觸發(fā)異常時(shí)執(zhí)行,循環(huán)的 else 子句則在未運(yùn)行 break 時(shí)執(zhí)行。try 語(yǔ)句和異常詳見(jiàn) 異常的處理。

continue 語(yǔ)句也借鑒自 C 語(yǔ)言,表示繼續(xù)執(zhí)行循環(huán)的下一次迭代:

>>>
>>> for num in range(2, 10):
...     if num % 2 == 0:
...         print("Found an even number", num)
...         continue
...     print("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. pass 語(yǔ)句?

pass 語(yǔ)句不執(zhí)行任何操作。語(yǔ)法上需要一個(gè)語(yǔ)句,但程序不實(shí)際執(zhí)行任何動(dòng)作時(shí),可以使用該語(yǔ)句。例如:

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

下面這段代碼創(chuàng)建了一個(gè)最小的類:

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

pass 還可以用作函數(shù)或條件子句的占位符,讓開(kāi)發(fā)者聚焦更抽象的層次。此時(shí),程序直接忽略 pass

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

4.6. match 語(yǔ)句?

A match statement takes an expression and compares its value to successive patterns given as one or more case blocks. This is superficially similar to a switch statement in C, Java or JavaScript (and many other languages), but it can also extract components (sequence elements or object attributes) from the value into variables.

最簡(jiǎn)單的形式是將一個(gè)目標(biāo)值與一個(gè)或多個(gè)字面值進(jìn)行比較:

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"

注意最后一個(gè)代碼塊:“變量名” _ 被作為 通配符 并必定會(huì)匹配成功。 如果沒(méi)有 case 語(yǔ)句匹配成功,則不會(huì)執(zhí)行任何分支。

使用 | (“ or ”)在一個(gè)模式中可以組合多個(gè)字面值:

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

請(qǐng)仔細(xì)研究此代碼! 第一個(gè)模式有兩個(gè)字面值,可以看作是上面所示字面值模式的擴(kuò)展。但接下來(lái)的兩個(gè)模式結(jié)合了一個(gè)字面值和一個(gè)變量,而變量 綁定 了一個(gè)來(lái)自目標(biāo)的值(point)。第四個(gè)模式捕獲了兩個(gè)值,這使得它在概念上類似于解包賦值 (x, y) = point

如果使用類實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu),可在類名后加一個(gè)類似于構(gòu)造器的參數(shù)列表,這樣做可以把屬性放到變量里:

class Point:
    x: int
    y: int

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

可在 dataclass 等支持屬性排序的內(nèi)置類中使用位置參數(shù)。還可在類中設(shè)置 __match_args__ 特殊屬性為模式的屬性定義指定位置。如果它被設(shè)為 ("x", "y"),則以下模式均為等價(jià)的,并且都把 y 屬性綁定到 var 變量:

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

讀取模式的推薦方式是將它們看做是你會(huì)在賦值操作左側(cè)放置的內(nèi)容的擴(kuò)展形式,以便理解各個(gè)變量將會(huì)被設(shè)置的值。 只有單獨(dú)的名稱(例如上面的 var)會(huì)被 match 語(yǔ)句所賦值。 帶點(diǎn)號(hào)的名稱 (例如 foo.bar)、屬性名稱(例如上面的 x=y=)或類名稱(通過(guò)其后的 "(...)" 來(lái)識(shí)別,例如上面的 Point)都絕不會(huì)被賦值。

模式可以任意地嵌套。例如,如果有一個(gè)由點(diǎn)組成的短列表,則可使用如下方式進(jìn)行匹配:

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

為模式添加成為守護(hù)項(xiàng)的 if 子句。如果守護(hù)項(xiàng)的值為假,則 match 繼續(xù)匹配下一個(gè) case 語(yǔ)句塊。注意,值的捕獲發(fā)生在守護(hù)項(xiàng)被求值之前:

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

match 語(yǔ)句的其他特性:

  • 與解包賦值類似,元組和列表模式具有完全相同的含義,并且實(shí)際上能匹配任意序列。 但它們不能匹配迭代器或字符串。

  • 序列模式支持?jǐn)U展解包操作:[x, y, *rest](x, y, *rest) 的作用類似于解包賦值。 在 * 之后的名稱也可以為 _,因此,(x, y, *_) 可以匹配包含至少兩個(gè)條目的序列,而不必綁定其余的條目。

  • Mapping patterns: {"bandwidth": b, "latency": l} captures the "bandwidth" and "latency" values from a dictionary. Unlike sequence patterns, extra keys are ignored. An unpacking like **rest is also supported. (But **_ would be redundant, so it is not allowed.)

  • 使用 as 關(guān)鍵字可以捕獲子模式:

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

    將把輸入的第二個(gè)元素捕獲為 p2 (只要輸入是包含兩個(gè)點(diǎn)的序列)

  • 大多數(shù)字面值是按相等性比較的,但是單例對(duì)象 True, FalseNone 則是按標(biāo)識(shí)號(hào)比較的。

  • 模式可以使用命名常量。 這些命名常量必須為帶點(diǎn)號(hào)的名稱以防止它們被解讀為捕獲變量:

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

要獲取更詳細(xì)的說(shuō)明和額外的示例,你可以參閱以教程格式撰寫(xiě)的 PEP 636。

4.7. 定義函數(shù)?

下列代碼創(chuàng)建一個(gè)可以輸出限定數(shù)值內(nèi)的斐波那契數(shù)列函數(shù):

>>>
>>> def fib(n):    # write Fibonacci series up to n
...     """Print a Fibonacci series up to 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

定義 函數(shù)使用關(guān)鍵字 def,后跟函數(shù)名與括號(hào)內(nèi)的形參列表。函數(shù)語(yǔ)句從下一行開(kāi)始,并且必須縮進(jìn)。

函數(shù)內(nèi)的第一條語(yǔ)句是字符串時(shí),該字符串就是文檔字符串,也稱為 docstring,詳見(jiàn) 文檔字符串。利用文檔字符串可以自動(dòng)生成在線文檔或打印版文檔,還可以讓開(kāi)發(fā)者在瀏覽代碼時(shí)直接查閱文檔;Python 開(kāi)發(fā)者最好養(yǎng)成在代碼中加入文檔字符串的好習(xí)慣。

函數(shù)在 執(zhí)行 時(shí)使用函數(shù)局部變量符號(hào)表,所有函數(shù)變量賦值都存在局部符號(hào)表中;引用變量時(shí),首先,在局部符號(hào)表里查找變量,然后,是外層函數(shù)局部符號(hào)表,再是全局符號(hào)表,最后是內(nèi)置名稱符號(hào)表。因此,盡管可以引用全局變量和外層函數(shù)的變量,但最好不要在函數(shù)內(nèi)直接賦值(除非是 global 語(yǔ)句定義的全局變量,或 nonlocal 語(yǔ)句定義的外層函數(shù)變量)。

在調(diào)用函數(shù)時(shí)會(huì)將實(shí)際參數(shù)(實(shí)參)引入到被調(diào)用函數(shù)的局部符號(hào)表中;因此,實(shí)參是使用 按值調(diào)用 來(lái)傳遞的(其中的 始終是對(duì)象的 引用 而不是對(duì)象的值)。 1 當(dāng)一個(gè)函數(shù)調(diào)用另外一個(gè)函數(shù)時(shí),會(huì)為該調(diào)用創(chuàng)建一個(gè)新的局部符號(hào)表。

函數(shù)定義在當(dāng)前符號(hào)表中把函數(shù)名與函數(shù)對(duì)象關(guān)聯(lián)在一起。解釋器把函數(shù)名指向的對(duì)象作為用戶自定義函數(shù)。還可以使用其他名稱指向同一個(gè)函數(shù)對(duì)象,并訪問(wèn)訪該函數(shù):

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

fib 不返回值,因此,其他語(yǔ)言不把它當(dāng)作函數(shù),而是當(dāng)作過(guò)程。事實(shí)上,沒(méi)有 return 語(yǔ)句的函數(shù)也返回值,只不過(guò)這個(gè)值比較是 None (是一個(gè)內(nèi)置名稱)。一般來(lái)說(shuō),解釋器不會(huì)輸出單獨(dú)的返回值 None ,如需查看該值,可以使用 print()

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

編寫(xiě)不直接輸出斐波那契數(shù)列運(yùn)算結(jié)果,而是返回運(yùn)算結(jié)果列表的函數(shù)也非常簡(jiǎn)單:

>>>
>>> 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 語(yǔ)句返回函數(shù)的值。return 語(yǔ)句不帶表達(dá)式參數(shù)時(shí),返回 None。函數(shù)執(zhí)行完畢退出也返回 None

  • result.append(a) 語(yǔ)句調(diào)用了列表對(duì)象 result方法 。方法是“從屬于”對(duì)象的函數(shù),命名為 obj.methodname,obj 是對(duì)象(也可以是表達(dá)式),methodname 是對(duì)象類型定義的方法名。不同類型定義不同的方法,不同類型的方法名可以相同,且不會(huì)引起歧義。(用 可以自定義對(duì)象類型和方法,詳見(jiàn) )示例中的方法 append() 是為列表對(duì)象定義的,用于在列表末尾添加新元素。本例中,該方法相當(dāng)于 result = result + [a] ,但更有效。

4.8. 函數(shù)定義詳解?

函數(shù)定義支持可變數(shù)量的參數(shù)。這里列出三種可以組合使用的形式。

4.8.1. 默認(rèn)值參數(shù)?

為參數(shù)指定默認(rèn)值是非常有用的方式。調(diào)用函數(shù)時(shí),可以使用比定義時(shí)更少的參數(shù),例如:

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

該函數(shù)可以用以下方式調(diào)用:

  • 只給出必選實(shí)參:ask_ok('Do you really want to quit?')

  • 給出一個(gè)可選實(shí)參:ask_ok('OK to overwrite the file?', 2)

  • 給出所有實(shí)參:ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')

本例還使用了關(guān)鍵字 in ,用于確認(rèn)序列中是否包含某個(gè)值。

默認(rèn)值在 定義 作用域里的函數(shù)定義中求值,所以:

i = 5

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

i = 6
f()

上例輸出的是 5。

重要警告: 默認(rèn)值只計(jì)算一次。默認(rèn)值為列表、字典或類實(shí)例等可變對(duì)象時(shí),會(huì)產(chǎn)生與該規(guī)則不同的結(jié)果。例如,下面的函數(shù)會(huì)累積后續(xù)調(diào)用時(shí)傳遞的參數(shù):

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

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

輸出結(jié)果如下:

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

不想在后續(xù)調(diào)用之間共享默認(rèn)值時(shí),應(yīng)以如下方式編寫(xiě)函數(shù):

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

4.8.2. 關(guān)鍵字參數(shù)?

kwarg=value 形式的 關(guān)鍵字參數(shù) 也可以用于調(diào)用函數(shù)。函數(shù)示例如下:

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

該函數(shù)接受一個(gè)必選參數(shù)(voltage)和三個(gè)可選參數(shù)(state, actiontype)。該函數(shù)可用下列方式調(diào)用:

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

以下調(diào)用函數(shù)的方式都無(wú)效:

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

函數(shù)調(diào)用時(shí),關(guān)鍵字參數(shù)必須跟在位置參數(shù)后面。所有傳遞的關(guān)鍵字參數(shù)都必須匹配一個(gè)函數(shù)接受的參數(shù)(比如,actor 不是函數(shù) parrot 的有效參數(shù)),關(guān)鍵字參數(shù)的順序并不重要。這也包括必選參數(shù),(比如,parrot(voltage=1000) 也有效)。不能對(duì)同一個(gè)參數(shù)多次賦值,下面就是一個(gè)因此限制而失敗的例子:

>>>
>>> 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'

最后一個(gè)形參為 **name 形式時(shí),接收一個(gè)字典(詳見(jiàn) 映射類型 --- dict),該字典包含與函數(shù)中已定義形參對(duì)應(yīng)之外的所有關(guān)鍵字參數(shù)。**name 形參可以與 *name 形參(下一小節(jié)介紹)組合使用(*name 必須在 **name 前面), *name 形參接收一個(gè) 元組,該元組包含形參列表之外的位置參數(shù)。例如,可以定義下面這樣的函數(shù):

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

該函數(shù)可以用如下方式調(diào)用:

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

輸出結(jié)果如下:

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

注意,關(guān)鍵字參數(shù)在輸出結(jié)果中的順序與調(diào)用函數(shù)時(shí)的順序一致。

4.8.3. 特殊參數(shù)?

默認(rèn)情況下,參數(shù)可以按位置或顯式關(guān)鍵字傳遞給 Python 函數(shù)。為了讓代碼易讀、高效,最好限制參數(shù)的傳遞方式,這樣,開(kāi)發(fā)者只需查看函數(shù)定義,即可確定參數(shù)項(xiàng)是僅按位置、按位置或關(guān)鍵字,還是僅按關(guān)鍵字傳遞。

函數(shù)定義如下:

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

/* 是可選的。這些符號(hào)表明形參如何把參數(shù)值傳遞給函數(shù):位置、位置或關(guān)鍵字、關(guān)鍵字。關(guān)鍵字形參也叫作命名形參。

4.8.3.1. 位置或關(guān)鍵字參數(shù)?

函數(shù)定義中未使用 /* 時(shí),參數(shù)可以按位置或關(guān)鍵字傳遞給函數(shù)。

4.8.3.2. 僅位置參數(shù)?

此處再介紹一些細(xì)節(jié),特定形參可以標(biāo)記為 僅限位置。僅限位置 時(shí),形參的順序很重要,且這些形參不能用關(guān)鍵字傳遞。僅限位置形參應(yīng)放在 / (正斜杠)前。/ 用于在邏輯上分割僅限位置形參與其它形參。如果函數(shù)定義中沒(méi)有 /,則表示沒(méi)有僅限位置形參。

/ 后可以是 位置或關(guān)鍵字僅限關(guān)鍵字 形參。

4.8.3.3. 僅限關(guān)鍵字參數(shù)?

把形參標(biāo)記為 僅限關(guān)鍵字,表明必須以關(guān)鍵字參數(shù)形式傳遞該形參,應(yīng)在參數(shù)列表中第一個(gè) 僅限關(guān)鍵字 形參前添加 *。

4.8.3.4. 函數(shù)示例?

請(qǐng)看下面的函數(shù)定義示例,注意 /* 標(biāo)記:

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

第一個(gè)函數(shù)定義 standard_arg 是最常見(jiàn)的形式,對(duì)調(diào)用方式?jīng)]有任何限制,可以按位置也可以按關(guān)鍵字傳遞參數(shù):

>>>
>>> standard_arg(2)
2

>>> standard_arg(arg=2)
2

第二個(gè)函數(shù) pos_only_arg 的函數(shù)定義中有 /,僅限使用位置形參:

>>>
>>> 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'

第三個(gè)函數(shù) kwd_only_args 的函數(shù)定義通過(guò) * 表明僅限關(guān)鍵字參數(shù):

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

最后一個(gè)函數(shù)在同一個(gè)函數(shù)定義中,使用了全部三種調(diào)用慣例:

>>>
>>> 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'

下面的函數(shù)定義中,kwdsname 當(dāng)作鍵,因此,可能與位置參數(shù) name 產(chǎn)生潛在沖突:

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

調(diào)用該函數(shù)不可能返回 True,因?yàn)殛P(guān)鍵字 'name' 總與第一個(gè)形參綁定。例如:

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

加上 / (僅限位置參數(shù))后,就可以了。此時(shí),函數(shù)定義把 name 當(dāng)作位置參數(shù),'name' 也可以作為關(guān)鍵字參數(shù)的鍵:

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

換句話說(shuō),僅限位置形參的名稱可以在 **kwds 中使用,而不產(chǎn)生歧義。

4.8.3.5. 小結(jié)?

以下用例決定哪些形參可以用于函數(shù)定義:

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

說(shuō)明:

  • 使用僅限位置形參,可以讓用戶無(wú)法使用形參名。形參名沒(méi)有實(shí)際意義時(shí),強(qiáng)制調(diào)用函數(shù)的實(shí)參順序時(shí),或同時(shí)接收位置形參和關(guān)鍵字時(shí),這種方式很有用。

  • 當(dāng)形參名有實(shí)際意義,且顯式名稱可以讓函數(shù)定義更易理解時(shí),阻止用戶依賴傳遞實(shí)參的位置時(shí),才使用關(guān)鍵字。

  • 對(duì)于 API,使用僅限位置形參,可以防止未來(lái)修改形參名時(shí)造成破壞性的 API 變動(dòng)。

4.8.4. 任意實(shí)參列表?

調(diào)用函數(shù)時(shí),使用任意數(shù)量的實(shí)參是最少見(jiàn)的選項(xiàng)。這些實(shí)參包含在元組中(詳見(jiàn) 元組和序列 )。在可變數(shù)量的實(shí)參之前,可能有若干個(gè)普通參數(shù):

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

Normally, these variadic arguments will be last in the list of formal parameters, because they scoop up all remaining input arguments that are passed to the function. Any formal parameters which occur after the *args parameter are 'keyword-only' arguments, meaning that they can only be used as keywords rather than positional arguments.

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

4.8.5. 解包實(shí)參列表?

函數(shù)調(diào)用要求獨(dú)立的位置參數(shù),但實(shí)參在列表或元組里時(shí),要執(zhí)行相反的操作。例如,內(nèi)置的 range() 函數(shù)要求獨(dú)立的 startstop 實(shí)參。如果這些參數(shù)不是獨(dú)立的,則要在調(diào)用函數(shù)時(shí),用 * 操作符把實(shí)參從列表或元組解包出來(lái):

>>>
>>> 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]

同樣,字典可以用 ** 操作符傳遞關(guān)鍵字參數(shù):

>>>
>>> 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.8.6. Lambda 表達(dá)式?

lambda 關(guān)鍵字用于創(chuàng)建小巧的匿名函數(shù)。lambda a, b: a+b 函數(shù)返回兩個(gè)參數(shù)的和。Lambda 函數(shù)可用于任何需要函數(shù)對(duì)象的地方。在語(yǔ)法上,匿名函數(shù)只能是單個(gè)表達(dá)式。在語(yǔ)義上,它只是常規(guī)函數(shù)定義的語(yǔ)法糖。與嵌套函數(shù)定義一樣,lambda 函數(shù)可以引用包含作用域中的變量:

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

上例用 lambda 表達(dá)式返回函數(shù)。還可以把匿名函數(shù)用作傳遞的實(shí)參:

>>>
>>> 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.8.7. 文檔字符串?

以下是文檔字符串內(nèi)容和格式的約定。

第一行應(yīng)為對(duì)象用途的簡(jiǎn)短摘要。為保持簡(jiǎn)潔,不要在這里顯式說(shuō)明對(duì)象名或類型,因?yàn)榭赏ㄟ^(guò)其他方式獲取這些信息(除非該名稱碰巧是描述函數(shù)操作的動(dòng)詞)。這一行應(yīng)以大寫(xiě)字母開(kāi)頭,以句點(diǎn)結(jié)尾。

文檔字符串為多行時(shí),第二行應(yīng)為空白行,在視覺(jué)上將摘要與其余描述分開(kāi)。后面的行可包含若干段落,描述對(duì)象的調(diào)用約定、副作用等。

Python 解析器不會(huì)刪除 Python 中多行字符串字面值的縮進(jìn),因此,文檔處理工具應(yīng)在必要時(shí)刪除縮進(jìn)。這項(xiàng)操作遵循以下約定:文檔字符串第一行 之后 的第一個(gè)非空行決定了整個(gè)文檔字符串的縮進(jìn)量(第一行通常與字符串開(kāi)頭的引號(hào)相鄰,其縮進(jìn)在字符串中并不明顯,因此,不能用第一行的縮進(jìn)),然后,刪除字符串中所有行開(kāi)頭處與此縮進(jìn)“等價(jià)”的空白符。不能有比此縮進(jìn)更少的行,但如果出現(xiàn)了縮進(jìn)更少的行,應(yīng)刪除這些行的所有前導(dǎo)空白符。轉(zhuǎn)化制表符后(通常為 8 個(gè)空格),應(yīng)測(cè)試空白符的等效性。

下面是多行文檔字符串的一個(gè)例子:

>>>
>>> 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.8.8. 函數(shù)注解?

函數(shù)注解 是可選的用戶自定義函數(shù)類型的元數(shù)據(jù)完整信息(詳見(jiàn) PEP 3107PEP 484 )。

標(biāo)注 以字典的形式存放在函數(shù)的 __annotations__ 屬性中,并且不會(huì)影響函數(shù)的任何其他部分。 形參標(biāo)注的定義方式是在形參名后加冒號(hào),后面跟一個(gè)表達(dá)式,該表達(dá)式會(huì)被求值為標(biāo)注的值。 返回值標(biāo)注的定義方式是加組合符號(hào) ->,后面跟一個(gè)表達(dá)式,該標(biāo)注位于形參列表和表示 def 語(yǔ)句結(jié)束的冒號(hào)之間。 下面的示例有一個(gè)必須的參數(shù),一個(gè)可選的關(guān)鍵字參數(shù)以及返回值都帶有相應(yīng)的標(biāo)注:

>>>
>>> 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.9. 小插曲:編碼風(fēng)格?

現(xiàn)在你將要寫(xiě)更長(zhǎng),更復(fù)雜的 Python 代碼,是時(shí)候討論一下 代碼風(fēng)格 了。 大多數(shù)語(yǔ)言都能以不同的風(fēng)格被編寫(xiě)(或更準(zhǔn)確地說(shuō),被格式化);有些比其他的更具有可讀性。 能讓其他人輕松閱讀你的代碼總是一個(gè)好主意,采用一種好的編碼風(fēng)格對(duì)此有很大幫助。

Python 項(xiàng)目大多都遵循 PEP 8 的風(fēng)格指南;它推行的編碼風(fēng)格易于閱讀、賞心悅目。Python 開(kāi)發(fā)者均應(yīng)抽時(shí)間悉心研讀;以下是該提案中的核心要點(diǎn):

  • 縮進(jìn),用 4 個(gè)空格,不要用制表符。

    4 個(gè)空格是小縮進(jìn)(更深嵌套)和大縮進(jìn)(更易閱讀)之間的折中方案。制表符會(huì)引起混亂,最好別用。

  • 換行,一行不超過(guò) 79 個(gè)字符。

    這樣換行的小屏閱讀體驗(yàn)更好,還便于在大屏顯示器上并排閱讀多個(gè)代碼文件。

  • 用空行分隔函數(shù)和類,及函數(shù)內(nèi)較大的代碼塊。

  • 最好把注釋放到單獨(dú)一行。

  • 使用文檔字符串。

  • 運(yùn)算符前后、逗號(hào)后要用空格,但不要直接在括號(hào)內(nèi)使用: a = f(1, 2) + g(3, 4)。

  • 類和函數(shù)的命名要一致;按慣例,命名類用 UpperCamelCase,命名函數(shù)與方法用 lowercase_with_underscores。命名方法中第一個(gè)參數(shù)總是用 self (類和方法詳見(jiàn) 初探類)。

  • 編寫(xiě)用于國(guó)際多語(yǔ)環(huán)境的代碼時(shí),不要用生僻的編碼。Python 默認(rèn)的 UTF-8 或純 ASCII 可以勝任各種情況。

  • 同理,就算多語(yǔ)閱讀、維護(hù)代碼的可能再小,也不要在標(biāo)識(shí)符中使用非 ASCII 字符。

備注

1

實(shí)際上,對(duì)象引用調(diào)用 這種說(shuō)法更好,因?yàn)?,傳遞的是可變對(duì)象時(shí),調(diào)用者能發(fā)現(xiàn)被調(diào)者做出的任何更改(插入列表的元素)。