Python 入門指南—4. 深入 Python 流程控制

4。 深入 Python 流程控制

除了前面介紹的 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

語句。

4。2。for語句

Python 中的 for 語句和 C 或 Pascal 中的略有不同。通常的迴圈可能會依據一個等差數值步進過程(如 Pascal),或由使用者來定義迭代步驟和中止條件(如 C ),Python 的 for 語句依據任意序列(連結串列或字串)中的子項,按它們在序列中的順序來進行迭代。例如(沒有暗指):

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

在迭代過程中修改迭代序列不安全(只有在使用連結串列這樣的可變序列時才會有這樣的情況)。如果你想要修改你迭代的序列(例如,複製選擇項),你可以迭代它的複本。使用切割標識就可以很方便的做到這一點:

>>> for w in words[:]: # Loop over a slice copy of the entire list。。。。 if len(w) > 6:。。。 words。insert(0, w)。。。>>> words[‘defenestrate’, ‘cat’, ‘window’, ‘defenestrate’]

4。3。range()函式

如果你需要一個數值序列,內建函式 range() 會很方便,它生成一個等差級數連結串列:

>>> for i in range(5):。。。 print(i)。。。01234

range(10)

生成了一個包含 10 個值的連結串列,它用連結串列的索引值填充了這個長度為 10 的列表,所生成的連結串列中不包括範圍中的結束值。也可以讓 range() 操作從另一個數值開始,或者可以指定一個不同的步進值(甚至是負數,有時這也被稱為 “步長”):

range(5, 10) 5 through 9range(0, 10, 3) 0, 3, 6, 9range(-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 Mary1 had2 a3 little4 lamb

不過,這種場合可以方便的使用 enumerate(),請參見 迴圈技巧。

如果你只是列印一個序列的話會發生奇怪的事情:

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

在不同方面 range() 函式返回的物件表現為它是一個列表,但事實上它並不是。當你迭代它時,它是一個能夠像期望的序列返回連續項的物件;但為了節省空間,它並不真正構造列表。

我們稱此類物件是

可迭代的

,即適合作為那些期望從某些東西中獲得連續項直到結束的函式或結構的一個目標(引數)。我們已經見過的 for 語句就是這樣一個迭代器。list() 函式是另外一個(

迭代器

),它從可迭代(物件)中建立列表:

>>> list(range(5))[0, 1, 2, 3, 4]

稍後我們會看到更多返回可迭代(物件)和以可迭代(物件)作為引數的函式。

4。4。break和continue語句, 以及迴圈中的else子句

break 語句和 C 中的類似,用於跳出最近的一級 for 或 while 迴圈。

迴圈可以有一個

else

子句;它在迴圈迭代完整個列表(對於 for )或執行條件為 false (對於 while )時執行,但迴圈被 break 中止的情況下不會執行。以下搜尋素數的示例程式演示了這個子句:

>>> 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 number3 is a prime number4 equals 2 * 25 is a prime number6 equals 2 * 37 is a prime number8 equals 2 * 49 equals 3 * 3

(Yes, 這是正確的程式碼。看仔細:

else

語句是屬於 for 迴圈之中,

不是

if 語句。)

與迴圈一起使用時,

else

子句與 try 語句的

else

子句比與 if 語句的具有更多的共同點:try 語句的

else

子句在未出現異常時執行,迴圈的

else

子句在未出現

break

時執行。更多關於 try 語句和異常的內容,請參見 異常處理。

continue 語句是從 C 中借鑑來的,它表示迴圈繼續執行下一次迭代:

>>> for num in range(2, 10):。。。 if num % 2 == 0:。。。 print(“Found an even number”, num)。。。 continue。。。 print(“Found a number”, num)Found an even number 2Found a number 3Found an even number 4Found a number 5Found an even number 6Found a number 7Found an even number 8Found a number 9

4。5。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。6。 定義函式

我們可以建立一個用來生成指定邊界的斐波那契數列的函式:

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

關鍵字 def 引入了一個函式

定義

。在其後必須跟有函式名和包括形式引數的圓括號。函式體語句從下一行開始,必須是縮排的。

函式體的第一行語句可以是可選的字串文字,這個字串是函式的文件字串,或者稱為

docstring

。(更多關於 docstrings 的資訊請參考 文件字串) 有些工具透過 docstrings 自動生成線上的或可列印的文件,或者讓使用者透過程式碼互動瀏覽;在你的程式碼中包含 docstrings 是一個好的實踐,讓它成為習慣吧。

函式

呼叫

會為函式區域性變數生成一個新的符號表。確切的說,所有函式中的變數賦值都是將值儲存在區域性符號表。變數引用首先在區域性符號表中查詢,然後是包含函式的區域性符號表,然後是全域性符號表,最後是內建名字表。因此,全域性變數不能在函式中直接賦值(除非用 global 語句命名),儘管他們可以被引用。

函式引用的實際引數在函式呼叫時引入區域性符號表,因此,實參總是

傳值呼叫

(這裡的

總是一個物件 引用 ,而不是該物件的值)。[1] 一個函式被另一個函式呼叫時,一個新的區域性符號表在呼叫過程中被建立。

一個函式定義會在當前符號表內引入函式名。函式名指代的值(即函式體)有一個被 Python 直譯器認定為

使用者自定義函式

的型別。 這個值可以賦予其他的名字(即變數名),然後它也可以被當做函式使用。這可以作為通用的重新命名機制:

>>> fib>>> f = fib>>> f(100)0 1 1 2 3 5 8 13 21 34 55 89

如果你使用過其他語言,你可能會反對說:

fib

不是一個函式,而是一個方法,因為它並不返回任何值。事實上,沒有 return 語句的函式確實會返回一個值,雖然是一個相當令人厭煩的值(指 None )。這個值被稱為

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

稱為連結串列物件

result

的一個

方法

。方法是一個“屬於”某個物件的函式,它被命名為

obj。methodename

,這裡的

obj

是某個物件(可能是一個表示式),

methodename

是某個在該物件型別定義中的方法的命名。不同的型別定義不同的方法。不同型別可能有同樣名字的方法,但不會混淆。(當你定義自己的物件型別和方法時,可能會出現這種情況,

class

的定義方法詳見 類 )。示例中演示的

append()

方法由連結串列物件定義,它向連結串列中加入一個新元素。在示例中它等同於

result = result + [a]

,不過效率更高。

4。7。 深入 Python 函式定義

在 Python 中,你也可以定義包含若干引數的函式。這裡有三種可用的形式,也可以混合使用。

4。7。1。 預設引數值

最常用的一種形式是為一個或多個引數指定預設值。這會建立一個可以使用比定義時允許的引數更少的引數呼叫的函式,例如:

def ask_ok(prompt, retries=4, complaint=‘Yes or no, please!’): 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 OSError(‘uncooperative user’) print(complaint)

這個函式可以通過幾種不同的方式呼叫:

只給出必要的引數:

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 = 5def f(arg=i): print(arg)i = 6f()

將會輸出

5

重要警告:

預設值只被賦值一次。這使得當預設值是可變物件時會有所不同,比如列表、字典或者大多數類的例項。例如,下面的函式在後續呼叫過程中會累積(前面)傳給它的引數:

def f(a, L=[]): L。append(a) return Lprint(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。7。2。 關鍵字引數

函式可以透過 關鍵字引數 的形式來呼叫,形如

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

不過以下幾種呼叫是無效的:

parrot() # required argument missingparrot(voltage=5。0, ‘dead’) # non-keyword argument after a keyword argumentparrot(110, voltage=220) # duplicate value for the same argumentparrot(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 “”, line 1, in ?TypeError: function() got multiple values for keyword argument ‘a’

引入一個形如

**name

的引數時,它接收一個字典(參見 Mapping Types — 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) keys = sorted(keywords。keys()) for kw in keys: 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 LimburgerIt‘s very runny, sir。It’s really very, VERY runny, sir。————————————————————client : John Cleeseshopkeeper : Michael Palinsketch : Cheese Shop Sketch

注意在列印關鍵字引數之前,透過對關鍵字字典

keys()

方法的結果進行排序,生成了關鍵字引數名的列表;如果不這樣做,打印出來的引數的順序是未定義的。

4。7。3。 可變引數列表

最後,一個最不常用的選擇是可以讓函式呼叫可變個數的引數。這些引數被包裝進一個元組(參見 元組和序列 )。在這些可變個數的引數之前,可以有零到多個普通的引數:

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。7。4。 引數列表的分拆

另有一種相反的情況: 當你要傳遞的引數已經是一個列表,但要呼叫的函式卻接受分開一個個的引數值。這時候你要把已有的列表拆開來。例如內建函式 range() 需要要獨立的

start

stop

引數。你可以在呼叫函式時加一個

*

運算子來自動把引數列表拆開:

>>> 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。7。5。 Lambda 形式

出於實際需要,有幾種通常在函數語言程式設計語言例如 Lisp 中出現的功能加入到了 Python。透過 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。7。6。 文件字串

這裡介紹的文件字串的概念和格式。

第一行應該是關於物件用途的簡介。簡短起見,不用明確的陳述物件名或型別,因為它們可以從別的途徑瞭解到(除非這個名字碰巧就是描述這個函式操作的動詞)。這一行應該以大寫字母開頭,以句號結尾。

如果文件字串有多行,第二行應該空出來,與接下來的詳細描述明確分隔。接下來的文件應該有一或多段描述物件的呼叫約定、邊界效應等。

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。7。7。 函式註解

函式註解 是關於使用者自定義的函式的完全可選的、隨意的元資料資訊。無論 Python 本身或者標準庫中都沒有使用函式註解;本節只是描述了語法。第三方的專案是自由地為文件,型別檢查,以及其它用途選擇函式註解。

註解是以字典形式儲存在函式的

__annotations__

屬性中,對函式的其它部分沒有任何影響。引數註解(Parameter annotations)是定義在引數名稱的冒號後面,緊隨著一個用來表示註解的值得表示式。返回註釋(Return annotations)是定義在一個

->

後面,緊隨著一個表示式,在冒號與

->

之間。下面的示例包含一個位置引數,一個關鍵字引數,和沒有意義的返回值註釋:

>>> def f(ham: 42, eggs: int = ‘spam’) -> “Nothing to see here”:。。。 print(“Annotations:”, f。__annotations__)。。。 print(“Arguments:”, ham, eggs)。。。>>> f(‘wonderful’)Annotations: {‘eggs’: , ‘return’: ‘Nothing to see here’, ‘ham’: 42}Arguments: wonderful spam

4。8。 插曲:編碼風格

此時你已經可以寫一些更長更復雜的 Python 程式,是時候討論一下

編碼風格

了。大多數語言可以寫(或者更明白的說,

格式化

)作幾種不同的風格。有些比其它的更好讀。讓你的程式碼對別人更易讀是個好想法,養成良好的編碼風格對此很有幫助。

對於 Python,PEP 8 引入了大多數專案遵循的風格指導。它給出了一個高度可讀,視覺友好的編碼風格。每個 Python 開發者都應該讀一下,大多數要點都會對你有幫助:

使用 4 空格縮排,而非 TAB在小縮排(可以巢狀更深)和大縮排(更易讀)之間,4空格是一個很好的折中。TAB 引發了一些混亂,最好棄用

折行以確保其不會超過 79 個字元這有助於小顯示器使用者閱讀,也可以讓大顯示器能並排顯示幾個程式碼檔案

使用空行分隔函式和類,以及函式中的大塊程式碼

可能的話,註釋獨佔一行

使用文件字串

把空格放到運算子兩邊,以及逗號後面,但是括號裡側不加空格:

a = f(1, 2) + g(3, 4)

統一函式和類命名推薦類名用

駝峰命名

, 函式和方法名用

小寫_和_下劃線

。總是用

self

作為方法的第一個引數(關於類和方法的知識詳見 初識類 )

不要使用花哨的編碼,如果你的程式碼的目的是要在國際化環境。Python 的預設情況下,UTF-8,甚至普通的 ASCII 總是工作的最好

同樣,也不要使用非 ASCII 字元的識別符號,除非是不同語種的會閱讀或者維護程式碼。

Footnotes

[1]

實際上,

引用物件呼叫

描述的更為準確。如果傳入一個可變物件,呼叫者會看到呼叫操作帶來的任何變化(如子項插入到列表中)。