4. 其他流程控制工具?
除了上一章介紹的 while
語(yǔ)句,Python 還支持其他語(yǔ)言中常見(jiàn)的流程控制語(yǔ)句,只是稍有不同。
4.1. if
語(yǔ)句?
最讓人耳熟能詳的應該是 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
... 序列可以當作其他語(yǔ)言中 switch
或 case
語(yǔ)句的替代品。
如果要把一個(gè)值與多個(gè)常量進(jìn)行比較,或者檢查特定類(lèi)型或屬性,match
語(yǔ)句更實(shí)用。詳見(jiàn) match 語(yǔ)句。
4.2. for
語(yǔ)句?
Python 的 for
語(yǔ)句與 C 或 Pascal 中的不同。Python 的 for
語(yǔ)句不迭代算術(shù)遞增數值(如 Pascal),或是給予用戶(hù)定義迭代步驟和暫停條件的能力(如 C),而是迭代列表或字符串等任意序列,元素的迭代順序與在序列中出現的順序一致。 例如:
>>> # Measure some strings:
... words = ['cat', 'window', 'defenestrate']
>>> for w in words:
... print(w, len(w))
...
cat 3
window 6
defenestrate 12
遍歷集合時(shí)修改集合的內容,會(huì )很容易生成錯誤的結果。因此不能直接進(jìn)行循環(huán),而是應遍歷該集合的副本或創(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()
函數?
內置函數 range()
常用于遍歷數字序列,該函數可以生成算術(shù)級數:
>>> for i in range(5):
... print(i)
...
0
1
2
3
4
生成的序列不包含給定的終止數值;range(10)
生成 10 個(gè)值,這是一個(gè)長(cháng)度為 10 的序列,其中的元素索引都是合法的。range 可以不從 0 開(kāi)始,還可以按指定幅度遞增(遞增幅度稱(chēng)為 '步進(jìn)',支持負數):
>>> 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ò),大多數情況下,enumerate()
函數更便捷,詳見(jiàn) 循環(huán)的技巧 。
如果只輸出 range,會(huì )出現意想不到的結果:
>>> range(10)
range(0, 10)
range()
返回對象的操作和列表很像,但其實(shí)這兩種對象不是一回事。迭代時(shí),該對象基于所需序列返回連續項,并沒(méi)有生成真正的列表,從而節省了空間。
這種對象稱(chēng)為可迭代對象 iterable,函數或程序結構可通過(guò)該對象獲取連續項,直到所有元素全部迭代完畢。for
語(yǔ)句就是這樣的架構,sum()
是一種把可迭代對象作為參數的函數:
>>> sum(range(4)) # 0 + 1 + 2 + 3
6
下文將介紹更多返回可迭代對象或把可迭代對象當作參數的函數。 在 數據結構 這一章節中,我們將討論有關(guān) list()
的更多細節。
4.4. 循環(huán)中的 break
、continue
語(yǔ)句及 else
子句?
break
語(yǔ)句和 C 中的類(lèi)似,用于跳出最近的 for
或 while
循環(huán)。
循環(huán)語(yǔ)句支持 else
子句;for
循環(huán)中,可迭代對象中的元素全部循環(huán)完畢,或 while
循環(huán)的條件為假時(shí),執行該子句;break
語(yǔ)句終止循環(huán)時(shí),不執行該子句。 請看下面這個(gè)查找素數的循環(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)錯,這段代碼就是這么寫(xiě)。仔細看:else
子句屬于 for
循環(huán),不屬于 if
語(yǔ)句。)
與 if
語(yǔ)句相比,循環(huán)的 else
子句更像 try
的 else
子句: try
的 else
子句在未觸發(fā)異常時(shí)執行,循環(huán)的 else
子句則在未運行 break
時(shí)執行。try
語(yǔ)句和異常詳見(jiàn) 異常的處理。
continue
語(yǔ)句也借鑒自 C 語(yǔ)言,表示繼續執行循環(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ǔ)句不執行任何操作。語(yǔ)法上需要一個(gè)語(yǔ)句,但程序不實(shí)際執行任何動(dòng)作時(shí),可以使用該語(yǔ)句。例如:
>>> while True:
... pass # Busy-wait for keyboard interrupt (Ctrl+C)
...
下面這段代碼創(chuàng )建了一個(gè)最小的類(lèi):
>>> class MyEmptyClass:
... pass
...
pass
還可以用作函數或條件子句的占位符,讓開(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è)目標值與一個(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ì )執行任何分支。
使用 |
(“ or ”)在一個(gè)模式中可以組合多個(gè)字面值:
case 401 | 403 | 404:
return "Not allowed"
模式的形式類(lèi)似解包賦值,并可被用于綁定變量:
# 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")
請仔細研究此代碼! 第一個(gè)模式有兩個(gè)字面值,可以看作是上面所示字面值模式的擴展。但接下來(lái)的兩個(gè)模式結合了一個(gè)字面值和一個(gè)變量,而變量 綁定 了一個(gè)來(lái)自目標的值(point
)。第四個(gè)模式捕獲了兩個(gè)值,這使得它在概念上類(lèi)似于解包賦值 (x, y) = point
。
如果使用類(lèi)實(shí)現數據結構,可在類(lèi)名后加一個(gè)類(lèi)似于構造器的參數列表,這樣做可以把屬性放到變量里:
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 等支持屬性排序的內置類(lèi)中使用位置參數。還可在類(lèi)中設置 __match_args__
特殊屬性為模式的屬性定義指定位置。如果它被設為 ("x", "y"),則以下模式均為等價(jià)的,并且都把 y
屬性綁定到 var
變量:
Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)
讀取模式的推薦方式是將它們看做是你會(huì )在賦值操作左側放置的內容的擴展形式,以便理解各個(gè)變量將會(huì )被設置的值。 只有單獨的名稱(chēng)(例如上面的 var
)會(huì )被 match 語(yǔ)句所賦值。 帶點(diǎn)號的名稱(chēng) (例如 foo.bar
)、屬性名稱(chēng)(例如上面的 x=
和 y=
)或類(lèi)名稱(chēng)(通過(guò)其后的 "(...)" 來(lái)識別,例如上面的 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")
為模式添加成為守護項的 if
子句。如果守護項的值為假,則 match
繼續匹配下一個(gè) case 語(yǔ)句塊。注意,值的捕獲發(fā)生在守護項被求值之前:
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ǔ)句的其他特性:
與解包賦值類(lèi)似,元組和列表模式具有完全相同的含義,并且實(shí)際上能匹配任意序列。 但它們不能匹配迭代器或字符串。
序列模式支持擴展解包操作:
[x, y, *rest]
和(x, y, *rest)
的作用類(lèi)似于解包賦值。 在*
之后的名稱(chēng)也可以為_
,因此,(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)的序列)大多數字面值是按相等性比較的,但是單例對象
True
,False
和None
則是按標識號比較的。模式可以使用命名常量。 這些命名常量必須為帶點(diǎn)號的名稱(chēng)以防止它們被解讀為捕獲變量:
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 :(")
要獲取更詳細的說(shuō)明和額外的示例,你可以參閱以教程格式撰寫(xiě)的 PEP 636。
4.7. 定義函數?
下列代碼創(chuàng )建一個(gè)可以輸出限定數值內的斐波那契數列函數:
>>> 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
定義 函數使用關(guān)鍵字 def
,后跟函數名與括號內的形參列表。函數語(yǔ)句從下一行開(kāi)始,并且必須縮進(jìn)。
函數內的第一條語(yǔ)句是字符串時(shí),該字符串就是文檔字符串,也稱(chēng)為 docstring,詳見(jiàn) 文檔字符串。利用文檔字符串可以自動(dòng)生成在線(xiàn)文檔或打印版文檔,還可以讓開(kāi)發(fā)者在瀏覽代碼時(shí)直接查閱文檔;Python 開(kāi)發(fā)者最好養成在代碼中加入文檔字符串的好習慣。
函數在 執行 時(shí)使用函數局部變量符號表,所有函數變量賦值都存在局部符號表中;引用變量時(shí),首先,在局部符號表里查找變量,然后,是外層函數局部符號表,再是全局符號表,最后是內置名稱(chēng)符號表。因此,盡管可以引用全局變量和外層函數的變量,但最好不要在函數內直接賦值(除非是 global
語(yǔ)句定義的全局變量,或 nonlocal
語(yǔ)句定義的外層函數變量)。
在調用函數時(shí)會(huì )將實(shí)際參數(實(shí)參)引入到被調用函數的局部符號表中;因此,實(shí)參是使用 按值調用 來(lái)傳遞的(其中的 值 始終是對象的 引用 而不是對象的值)。 1 當一個(gè)函數調用另外一個(gè)函數時(shí),會(huì )為該調用創(chuàng )建一個(gè)新的局部符號表。
函數定義在當前符號表中把函數名與函數對象關(guān)聯(lián)在一起。解釋器把函數名指向的對象作為用戶(hù)自定義函數。還可以使用其他名稱(chēng)指向同一個(gè)函數對象,并訪(fǎng)問(wèn)訪(fǎng)該函數:
>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89
fib
不返回值,因此,其他語(yǔ)言不把它當作函數,而是當作過(guò)程。事實(shí)上,沒(méi)有 return
語(yǔ)句的函數也返回值,只不過(guò)這個(gè)值比較是 None
(是一個(gè)內置名稱(chēng))。一般來(lái)說(shuō),解釋器不會(huì )輸出單獨的返回值 None
,如需查看該值,可以使用 print()
:
>>> fib(0)
>>> print(fib(0))
None
編寫(xiě)不直接輸出斐波那契數列運算結果,而是返回運算結果列表的函數也非常簡(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ǔ)句返回函數的值。return
語(yǔ)句不帶表達式參數時(shí),返回None
。函數執行完畢退出也返回None
。result.append(a)
語(yǔ)句調用了列表對象result
的 方法 。方法是“從屬于”對象的函數,命名為obj.methodname
,obj
是對象(也可以是表達式),methodname
是對象類(lèi)型定義的方法名。不同類(lèi)型定義不同的方法,不同類(lèi)型的方法名可以相同,且不會(huì )引起歧義。(用 類(lèi) 可以自定義對象類(lèi)型和方法,詳見(jiàn) 類(lèi) )示例中的方法append()
是為列表對象定義的,用于在列表末尾添加新元素。本例中,該方法相當于result = result + [a]
,但更有效。
4.8. 函數定義詳解?
函數定義支持可變數量的參數。這里列出三種可以組合使用的形式。
4.8.1. 默認值參數?
為參數指定默認值是非常有用的方式。調用函數時(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í)參:
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
,用于確認序列中是否包含某個(gè)值。
默認值在 定義 作用域里的函數定義中求值,所以:
i = 5
def f(arg=i):
print(arg)
i = 6
f()
上例輸出的是 5
。
重要警告: 默認值只計算一次。默認值為列表、字典或類(lèi)實(shí)例等可變對象時(shí),會(huì )產(chǎn)生與該規則不同的結果。例如,下面的函數會(huì )累積后續調用時(shí)傳遞的參數:
def f(a, L=[]):
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
輸出結果如下:
[1]
[1, 2]
[1, 2, 3]
不想在后續調用之間共享默認值時(shí),應以如下方式編寫(xiě)函數:
def f(a, L=None):
if L is None:
L = []
L.append(a)
return L
4.8.2. 關(guān)鍵字參數?
kwarg=value
形式的 關(guān)鍵字參數 也可以用于調用函數。函數示例如下:
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, "!")
該函數接受一個(gè)必選參數(voltage
)和三個(gè)可選參數(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
以下調用函數的方式都無(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í),關(guān)鍵字參數必須跟在位置參數后面。所有傳遞的關(guān)鍵字參數都必須匹配一個(gè)函數接受的參數(比如,actor
不是函數 parrot
的有效參數),關(guān)鍵字參數的順序并不重要。這也包括必選參數,(比如,parrot(voltage=1000)
也有效)。不能對同一個(gè)參數多次賦值,下面就是一個(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) 映射類(lèi)型 --- dict),該字典包含與函數中已定義形參對應之外的所有關(guān)鍵字參數。**name
形參可以與 *name
形參(下一小節介紹)組合使用(*name
必須在 **name
前面), *name
形參接收一個(gè) 元組,該元組包含形參列表之外的位置參數。例如,可以定義下面這樣的函數:
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
注意,關(guān)鍵字參數在輸出結果中的順序與調用函數時(shí)的順序一致。
4.8.3. 特殊參數?
默認情況下,參數可以按位置或顯式關(guān)鍵字傳遞給 Python 函數。為了讓代碼易讀、高效,最好限制參數的傳遞方式,這樣,開(kāi)發(fā)者只需查看函數定義,即可確定參數項是僅按位置、按位置或關(guān)鍵字,還是僅按關(guān)鍵字傳遞。
函數定義如下:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only
/
和 *
是可選的。這些符號表明形參如何把參數值傳遞給函數:位置、位置或關(guān)鍵字、關(guān)鍵字。關(guān)鍵字形參也叫作命名形參。
4.8.3.1. 位置或關(guān)鍵字參數?
函數定義中未使用 /
和 *
時(shí),參數可以按位置或關(guān)鍵字傳遞給函數。
4.8.3.2. 僅位置參數?
此處再介紹一些細節,特定形參可以標記為 僅限位置。僅限位置 時(shí),形參的順序很重要,且這些形參不能用關(guān)鍵字傳遞。僅限位置形參應放在 /
(正斜杠)前。/
用于在邏輯上分割僅限位置形參與其它形參。如果函數定義中沒(méi)有 /
,則表示沒(méi)有僅限位置形參。
/
后可以是 位置或關(guān)鍵字 或 僅限關(guān)鍵字 形參。
4.8.3.3. 僅限關(guān)鍵字參數?
把形參標記為 僅限關(guān)鍵字,表明必須以關(guān)鍵字參數形式傳遞該形參,應在參數列表中第一個(gè) 僅限關(guān)鍵字 形參前添加 *
。
4.8.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)
第一個(gè)函數定義 standard_arg
是最常見(jiàn)的形式,對調用方式?jīng)]有任何限制,可以按位置也可以按關(guān)鍵字傳遞參數:
>>> standard_arg(2)
2
>>> standard_arg(arg=2)
2
第二個(gè)函數 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'
第三個(gè)函數 kwd_only_args
的函數定義通過(guò) *
表明僅限關(guān)鍵字參數:
>>> 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è)函數在同一個(gè)函數定義中,使用了全部三種調用慣例:
>>> 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
產(chǎn)生潛在沖突:
def foo(name, **kwds):
return 'name' in kwds
調用該函數不可能返回 True
,因為關(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í),函數定義把 name
當作位置參數,'name'
也可以作為關(guān)鍵字參數的鍵:
def foo(name, /, **kwds):
return 'name' in kwds
>>> foo(1, **{'name': 2})
True
換句話(huà)說(shuō),僅限位置形參的名稱(chēng)可以在 **kwds
中使用,而不產(chǎn)生歧義。
4.8.3.5. 小結?
以下用例決定哪些形參可以用于函數定義:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
說(shuō)明:
使用僅限位置形參,可以讓用戶(hù)無(wú)法使用形參名。形參名沒(méi)有實(shí)際意義時(shí),強制調用函數的實(shí)參順序時(shí),或同時(shí)接收位置形參和關(guān)鍵字時(shí),這種方式很有用。
當形參名有實(shí)際意義,且顯式名稱(chēng)可以讓函數定義更易理解時(shí),阻止用戶(hù)依賴(lài)傳遞實(shí)參的位置時(shí),才使用關(guān)鍵字。
對于 API,使用僅限位置形參,可以防止未來(lái)修改形參名時(shí)造成破壞性的 API 變動(dòng)。
4.8.4. 任意實(shí)參列表?
調用函數時(shí),使用任意數量的實(shí)參是最少見(jiàn)的選項。這些實(shí)參包含在元組中(詳見(jiàn) 元組和序列 )。在可變數量的實(shí)參之前,可能有若干個(gè)普通參數:
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í)參在列表或元組里時(shí),要執行相反的操作。例如,內置的 range()
函數要求獨立的 start 和 stop 實(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)鍵字參數:
>>> 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 表達式?
lambda
關(guān)鍵字用于創(chuàng )建小巧的匿名函數。lambda a, b: a+b
函數返回兩個(gè)參數的和。Lambda 函數可用于任何需要函數對象的地方。在語(yǔ)法上,匿名函數只能是單個(gè)表達式。在語(yǔ)義上,它只是常規函數定義的語(yǔ)法糖。與嵌套函數定義一樣,lambda 函數可以引用包含作用域中的變量:
>>> def make_incrementor(n):
... return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43
上例用 lambda 表達式返回函數。還可以把匿名函數用作傳遞的實(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. 文檔字符串?
以下是文檔字符串內容和格式的約定。
第一行應為對象用途的簡(jiǎn)短摘要。為保持簡(jiǎn)潔,不要在這里顯式說(shuō)明對象名或類(lèi)型,因為可通過(guò)其他方式獲取這些信息(除非該名稱(chēng)碰巧是描述函數操作的動(dòng)詞)。這一行應以大寫(xiě)字母開(kāi)頭,以句點(diǎn)結尾。
文檔字符串為多行時(shí),第二行應為空白行,在視覺(jué)上將摘要與其余描述分開(kāi)。后面的行可包含若干段落,描述對象的調用約定、副作用等。
Python 解析器不會(huì )刪除 Python 中多行字符串字面值的縮進(jìn),因此,文檔處理工具應在必要時(shí)刪除縮進(jìn)。這項操作遵循以下約定:文檔字符串第一行 之后 的第一個(gè)非空行決定了整個(gè)文檔字符串的縮進(jìn)量(第一行通常與字符串開(kāi)頭的引號相鄰,其縮進(jìn)在字符串中并不明顯,因此,不能用第一行的縮進(jìn)),然后,刪除字符串中所有行開(kāi)頭處與此縮進(jìn)“等價(jià)”的空白符。不能有比此縮進(jìn)更少的行,但如果出現了縮進(jìn)更少的行,應刪除這些行的所有前導空白符。轉化制表符后(通常為 8 個(gè)空格),應測試空白符的等效性。
下面是多行文檔字符串的一個(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. 函數注解?
函數注解 是可選的用戶(hù)自定義函數類(lèi)型的元數據完整信息(詳見(jiàn) PEP 3107 和 PEP 484 )。
標注 以字典的形式存放在函數的 __annotations__
屬性中,并且不會(huì )影響函數的任何其他部分。 形參標注的定義方式是在形參名后加冒號,后面跟一個(gè)表達式,該表達式會(huì )被求值為標注的值。 返回值標注的定義方式是加組合符號 ->
,后面跟一個(gè)表達式,該標注位于形參列表和表示 def
語(yǔ)句結束的冒號之間。 下面的示例有一個(gè)必須的參數,一個(gè)可選的關(guān)鍵字參數以及返回值都帶有相應的標注:
>>> 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ě)更長(cháng),更復雜的 Python 代碼,是時(shí)候討論一下 代碼風(fēng)格 了。 大多數語(yǔ)言都能以不同的風(fēng)格被編寫(xiě)(或更準確地說(shuō),被格式化);有些比其他的更具有可讀性。 能讓其他人輕松閱讀你的代碼總是一個(gè)好主意,采用一種好的編碼風(fēng)格對此有很大幫助。
Python 項目大多都遵循 PEP 8 的風(fēng)格指南;它推行的編碼風(fēng)格易于閱讀、賞心悅目。Python 開(kāi)發(fā)者均應抽時(shí)間悉心研讀;以下是該提案中的核心要點(diǎn):
縮進(jìn),用 4 個(gè)空格,不要用制表符。
4 個(gè)空格是小縮進(jìn)(更深嵌套)和大縮進(jìn)(更易閱讀)之間的折中方案。制表符會(huì )引起混亂,最好別用。
換行,一行不超過(guò) 79 個(gè)字符。
這樣換行的小屏閱讀體驗更好,還便于在大屏顯示器上并排閱讀多個(gè)代碼文件。
用空行分隔函數和類(lèi),及函數內較大的代碼塊。
最好把注釋放到單獨一行。
使用文檔字符串。
運算符前后、逗號后要用空格,但不要直接在括號內使用:
a = f(1, 2) + g(3, 4)
。類(lèi)和函數的命名要一致;按慣例,命名類(lèi)用
UpperCamelCase
,命名函數與方法用lowercase_with_underscores
。命名方法中第一個(gè)參數總是用self
(類(lèi)和方法詳見(jiàn) 初探類(lèi))。編寫(xiě)用于國際多語(yǔ)環(huán)境的代碼時(shí),不要用生僻的編碼。Python 默認的 UTF-8 或純 ASCII 可以勝任各種情況。
同理,就算多語(yǔ)閱讀、維護代碼的可能再小,也不要在標識符中使用非 ASCII 字符。
備注
- 1
實(shí)際上,對象引用調用 這種說(shuō)法更好,因為,傳遞的是可變對象時(shí),調用者能發(fā)現被調者做出的任何更改(插入列表的元素)。