Python迴圈結構用法
本文介紹python中的while迴圈、for迴圈。在python中for可以用於迴圈,也可用於另一種近親的列表解析,列表解析是python中非常重要的特性,詳細內容見後面的文章。
一般來說,python寫for迴圈比寫while更容易、方便,而且python中的for比while效率要更高,如果可以,用for而不是while。
while迴圈
python中的while/for迴圈和其它語言的while迴圈有些不一樣,它支援else分支。結構如下:
while <CONDITION>: CODE else: CODE_ELSE
注意,condition部分只能是表示式,不能是語句,所以condition中不能包含賦值語句,如while a = x:
是錯誤的。
while和for的else分支表示當正常退出while/for迴圈的時候所執行的程式碼分支。所謂正常退出,是指不是通過break跳出的情況,也就是正常把所有迴圈條件輪完的情況。這對於那些需要通過設定標誌位來判斷的情況來說非常方便 ,而標誌位通常是用於離開迴圈的時候,提供一個額外的標記、通知功能,比如退出迴圈時想找的資料是否找到。
例如搜尋一個列表,並在退出時告知是否找到。如果使用標誌位來實現,如下:
found = False while x and not found: if match(x[0]): print("found it") found = True else: x = x[1:] if not found: print("not found")
如果通過else,則邏輯更清晰:
while x: if match(x[0]): print("found it") break x = x[1:] else: print("not found")
再例如,判斷一個數(如下面的y)是否是質數。
y = 21 x = y // 2 while x > 1: if y % x == 0: print( y, "has a factor: ", x) break x -= 1 else: print("y is a prime")
想象一下如果不使用while的else,上面的功能該如何實現。
pass、break、continue、else
這幾個關鍵字都能用在while/for中。
- break:退出整個迴圈(while/for),如果嵌套了迴圈,則退出break所在的那個層次
- continue:直接跳到下一次迴圈
- else:在迴圈正常退出(不是break中斷的迴圈)時執行的所執行的預設程式碼塊
-
pass:在python中作為空的佔位符,表示什麼也不做。比如:
- if x:pass
- while x:pass
- def x():pass
- class x:pass
在python 3.x中,pass的另一種方式是...
,它也表示什麼也不做的佔位符。
for迴圈
python中的for是一個通用的序列迭代器,和bash的for語法類似。python中沒有for(i=0;i<N;i++)
的語法,但for結合range可以實現一樣的功能,後文介紹。
for語法:
for i in <Sequence>: CODE else: CODE_ELSE
每次迭代時,for從序列中取一個元素賦值給控制變數i,下一輪迭代取下一個元素再賦值給i。和其它語言不太一樣,for中的控制變數不會在for迴圈完後消失,它會保持最後一個被迭代的元素值 。之所以會這樣,是因為其它語言中for是一個程式碼塊,而python中for不算是程式碼塊,也就是說沒有自己的名稱空間。
實際上不止序列,只要是可迭代的物件,都能用for進行遍歷。關於什麼是可迭代的,將專門在迭代器相關的文章中解釋。
例如,遍歷一個字串,因為它是序列。
for i in 'xiaofang': print(i) print("var i after: ",i)# 輸出g
遍歷一個列表:
L = ["aa","bb","cc"] for i in L: print(i)
巢狀:
L = ["aa","bb","cc"] for i in L: for j in i: print(j)
計算序列中所有數值的和:
L = [1,2,3,4,5] sum = 0 for i in L: sum += i print(sum)
for迭代字典
for迭代字典時,迭代的是key。
D = {'a': 1, 'b': 2, 'c': 3} for key in D: print(key, "=>", D[key])
其它迭代字典的幾種方式:
1.通過keys()迭代字典
for k in D.keys(): print(key, "=>", D[key])
2.直接迭代字典的value
for v in D.values(): print(v)
3.同時迭代key和value
for k, v in D.items(): print(k, v)
for中的賦值和序列解包
for迭代時,實際上是從可迭代物件中取元素並進行賦值的過程,python中各種變數賦值的方式在for中都支援。而且,python中變數賦值是按引用賦值的,所以每次迭代過程中賦值給控制變數的是那個元素的引用,而不是拷貝這個元素並賦值給控制變數 。所以,如果賦值給控制變數的是可變物件時,修改控制變數會直接修改原始資料。
例如:
T = [(1, 2), (3, 4), (5, 6)] for i in T: print(i) for (a, b) in T: print(a, b)
輸出:
(1, 2) (3, 4) (5, 6) 1 2 3 4 5 6
for還支援序列解包的賦值形式。
例如:
for (a, *b, c) in [(1, 2, 3, 4), (5, 6, 7, 8)]: print(a, b, c)
結果:
1 [2, 3] 4 5 [6, 7] 8
因為python是按引用賦值的,所以控制變數都是直接指向迭代元素的,而不是拷貝副本後進行賦值。看下面的結果:
L = [1111, 2222] print(id(L[0])) print(id(L[1])) print("-" * 15) for i in L: print(id(i))
輸出結果:
46990096 46990128 --------------- 46990096 46990128
可見,變數i和列表中元素的記憶體地址是一致的。
正因為是按引用賦值,所以迭代過程中修改賦值給控制變數i的不可變物件時會建立新物件,從而不會影響原始資料,但如果賦值給i的是可變物件,則修改i會影響原始資料。
例如:
L = [1111, 2222] for i in L: i += 1 print(L)
列表L不會改變:
[1111, 2222]
而下面修改控制變數i會改變原始物件:
L = [[1],[1,2],[1,2,3],[1,2,3,4]] for i in L: i.append(0) print(L)
結果:
[[1, 0], [1, 2, 0], [1, 2, 3, 0], [1, 2, 3, 4, 0]]
for + range
python中並沒有直接支援for i=0;i<N;i++
的for語法,但是,通過for + range(),可以實現類似的功能。
先介紹一下range()。它像Linux下的seq命令功能一樣,用來返回一些序列數值。range()返回一個迭代器,目前無需知道迭代器是什麼,只需知道它可以轉換成list、tuple、Set,然後可以在for中進行迭代。
>>> range(3) range(0, 3) >>> list(range(3)),set(range(3)),tuple(range(3)) ([0, 1, 2], {0, 1, 2}, (0, 1, 2))
可見,range()返回的序列值是前閉後開的。
還可以指定起始值,步進(每隔幾個數)。
>>> list(range(1,5)) [1, 2, 3, 4] >>> list(range(-1,5)) [-1, 0, 1, 2, 3, 4] >>> list(range(-1,5,2)) [-1, 1, 3]
步進值指定為負數的時候,可以生成降序的序列值。
>>> list(range(10,5,-1)) [10, 9, 8, 7, 6]
range()返回了生成序列值的迭代器後,可以用for來進行迭代。
for i in range(3): print(i)
range()還經常用於for中作為序列的索引位。例如:
L = ["a","b","c","d"] for i in range(3): print(L[i])
分析for + range迭代的過程
下面兩個例子,在結果上是等價的:
for i in range(3): print(i) for i in [0,1,2]: print(i)
但除了結果上,過程並不一樣。range()既然返回迭代器,說明序列數值是需要迭代一個臨時生成一個的,也就是說range()從始至終在記憶體中都只佔用一個數值的記憶體空間。而[0,1,2]
則是在記憶體中佔用一個包含3數值元素的列表,然後for從這個列表物件中按照索引進行迭代。
再通俗地解釋下,for i in range(3)
開始迭代的時候,生成一個數值0,第二次迭代再生成數值1,第三次迭代再生成數值2,在第一次迭代的時候,1和2都是不存在的。而[0,1,2]
則是早就存在於記憶體中,for通過list型別編寫好的迭代器進行迭代,每次迭代從已存在的數值中取一個元素。
所以,在效率上,使用range()要比直接解析列表要慢一點,但是在記憶體應用上,range()的方式要比直接解析已存在的列表要好,特別是列表較大的時候 。一般來說,python中最簡單的方式總是最好的、效率很大可能上也是最高的,所以能直接解析的時候,不使用range的效率總會更高一些。
這種效率的區別,也可以應用於其它迭代方式的分析上。例如,按行讀取檔案的兩種方式:
for i in open("filename"): print(i) for i in open("filename").readlines(): print(i)
第一種方式,open()返回一個檔案迭代器,每次需要迭代的時候才會去讀需要的那一行,也就是說從始至終在記憶體中都只佔用一行資料的空間。而第二種通過readlines()讀取時,它會一次性將檔案中所有行都讀取到一個列表中,然後for去迭代這個列表。如果檔案比較大,第二種方式可能會佔用比較大的記憶體,甚至可能比原檔案大小還要大,因為很可能會一次性為400M的檔案分配500M記憶體,以免後續不斷的記憶體分配。
for + range的步進以及分片
無論是range(),還是序列的分片計數,都支援步進。例如步進為2:
>>> list(range(1,6,2)) [1, 3, 5] >>> L = [1,2,3,4,5] >>> L[::2] [1, 3, 5]
它們都能用於for。
for i in range(1,6,2): print(i) L = [1,2,3,4,5] for i in L[::2]: print(i)
它們的結果是一樣的。但是和前面分析的一樣,range除了在記憶體應用上比較有優勢,在效率上是不及直接列表解析的,包括這裡分片步進。
for修改列表元素
有一個列表,想要為列表中的值都加1。
L = [1,2,3,4] for i in L: i += 1
這是無效的,雖然python中是按照引用進行賦值的,但數值型別是不可變型別,所以每次修改i實際上都會建立新的資料物件,並不會直接影響L中的元素。這些前文已經解釋過了。
如果想要修改L本身,直接迭代L是沒法實現的,可以通過迭代它的索引,然後通過索引的方式來修改L的元素值。例如:
L = [1,2,3,4] for i in range(len(L)): L[i] += 1 print(L)# 輸出:[2,3,4,5]
通過while也可以實現。但更簡單的方式是後面的文章要詳細解釋的"列表解析":
L = [1,2,3,4] L = [x + 1 for x in L] print(L)
for迭代的陷阱
for是一個通用的迭代器,它按照next的方式一次取一個元素,下一輪迭代取下一個元素。所以,如果在for內部修改了正在迭代的序列(所以這裡是說可變序列,且特指列表型別),可能會引起一些奇怪現象。
這是for的一個陷阱,或者說是迭代器的一個陷阱:迭代的物件在迭代過程中被修改了。
陷阱一
迭代操作是遞迴到資料物件中去的,而不是根據變數名進行迭代的。也就是說迭代的物件是記憶體中的資料物件。
例如:
L = [1,2,3,4] for i in L: ...
這個for迭代器在迭代剛開始的時候,先找到L所指向的迭代物件,即記憶體中的[1,2,3,4]
。如果迭代過程中如果L變成了一個集合,或另一個列表物件,for的迭代並不會收到影響。但如果是在原處修改這個列表,那麼迭代將會收到影響,例如新增元素也會被迭代到。
看下面的例子:
L = ['a','b','c','d','e'] ## 原處修改列表,新元素f、g也會被迭代 for i in L: if i in "de": L += ["f", "g"] print(i) ## 建立新列表,新元素f、g不會被迭代 for i in L: if i in "de": L = L + ["f", "g"] print(i)
陷阱二
例如,迭代一個列表 ,迭代過程中刪除一個列表元素。
L = ['a','b','c','d','e'] for i in L: if i in "bc": L.remove(i) print(i) print(L)
輸出的結果將是:
b ['a', 'c', 'd', 'e']
這個for迴圈的本意是想刪除b、c元素,但結果卻只刪除了b。通過結果可以發現,c根本就沒有被for迭代。之所以會這樣,是因為迭代到b的時候,滿足if條件,然後刪除了列表中的b元素。正因為刪除操作,使得列表中b後面的元素整體前移一個位置,也就是c元素的索引位置變成了index=1,而index=1的元素已經被for迭代過(即元素b),使得c幸運地逃過了for的迭代。
如果迭代並修改的是集合或字典呢?將會報錯。雖然它們是可變序列,但是它們是以hash key作為迭代依據的,只要增、刪元素,就會導致整個物件的順序hash key發生改變,這顯然是編寫這兩種型別的迭代器時所需要避免的問題 。如下:
D = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5} for i in D: if i in "bc": L.remove(i) print(i) print(L)
報錯:
b Traceback (most recent call last): File "g:/pycode/lists.py", line 12, in <module> for i in D: RuntimeError: dictionary changed size during iteration
S = {'a','b','c','d','e'} for i in S: if i in "bc": S.remove(i) print(i) print(S)
報錯:
b Traceback (most recent call last): File "g:/pycode/lists.py", line 4, in <module> for i in L: RuntimeError: Set changed size during iteration
迭代並修改集合、字典是非常常見的需求,但很多第三方模組在迭代並修改它們的時候都隱隱忽略了這種問題。那麼如何實現這種需求且不會出錯?可以考慮迭代它們的副本,並修改它們自身 。
例如:
D = {'a':1,'b':2,'c':3,'d':4,'e':5} for i in D.copy(): if i in "bc": D.pop(i) print(i) print(D) S = {'a','b','c','d','e'} for i in S.copy(): if i in "bc": S.remove(i) print(i) print(S)
結果:
b c {'a': 1, 'd': 4, 'e': 5} c b {'e', 'd', 'a'}
注意,別使用dict的keys()函式,在python 2.x是可以的,因為返回的是一個列表,但是在python 3.x中,它返回的是一個迭代器。
除了使用copy(),使用其它的方式也可以,只要保證迭代的物件和修改的物件不是同一個物件即可。例如,list()方法轉換Set/Dict,在轉換的過程中會建立新的資料物件,所以迭代和修改操作是互不影響的。
D = {'a':1,'b':2,'c':3,'d':4,'e':5} for i in list(D): if i in "bc": D.pop(i) print(i) print(D)