Python迴圈結構用法

駿馬金龍發表於2018-12-17

本文介紹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 + zip並行迭代

zip()函式可以將多個序列(實際上是更通用的可迭代物件)中的值一一對應地取出來,然後放進一個元組中。它也返回一個可迭代物件,可以直接通過list/set等函式將它們的內容一次性展現出來。

例如:

L = [1,2,3,4]
S = {`a`,`b`,`c`,`d`}

>>> zip(S,L)
<zip object at 0x03684148>
>>> list(zip(S,L))
[(`d`, 1), (`a`, 2), (`b`, 3), (`c`, 4)]

注意,集合是無序的,所以這裡從S中去的元素是隨機順序的。但無論如何,已經可以看出zip()的功能了:從容器1和容器2(可是更多個容器)中同時取出一個元素,組成元組返回,再取第二個元素返回。

>>> list(zip(L,L))
[(1, 1), (2, 2), (3, 3), (4, 4)]

如果容器中元素數量不等,則以長度最短的為基準進行截斷。例如:

L1 = [1,2,3,4,5]
L2 = [11,22,33,44,55,66]
L3 = [111,222,333]

>>> list(zip(L1,L2,L3))
[(1, 11, 111), (2, 22, 222), (3, 33, 333)]

zip()還常用於構造dict,例如:

keys = [`a`, `b`, `c`, `d`]
values = [1, 3, 5, 7]
D = dict(zip(keys, values))

>>> D
{`a`: 1, `b`: 3, `c`: 5, `d`: 7}

瞭解了zip(),就可以將它結合for來進行並行迭代:從每個zip()返回的元組中取來自各個容器中的元素。

例如:

L1 = [1,2,3,4,5]
L2 = [11,22,33,44,55,66]
L3 = [111,222,333]

for (x, y, z) in zip(L1,L2,L3):
    print("%d + %d + %d = %d" % (x, y, z, x + y + z))

結果:

1 + 11 + 111 = 123
2 + 22 + 222 = 246
3 + 33 + 333 = 369

enumerate()取得索引位和元素

在其他語言中,可能會有專門的工具在迭代每一個序列元素時同時取得這個元素的索引位和元素值。python中可以通過enumerate()來實現。

例如:

>>> L =  [`a`, `b`, `c`, `d`]

>>> list(enumerate(L))
[(0, `a`), (1, `b`), (2, `c`), (3, `d`)]

於是,可以通過for迭代器來迭代enumerate()生成的(index, value)元素:

for (k, v) in enumerate(L):
    print(k,v)

enumerate()還可以用它的第二個引數指定從哪個索引值開始標記索引。例如:

>>> list(enumerate(L, 2))
[(2, `a`), (3, `b`), (4, `c`), (5, `d`)]

需要注意的是,像dict這樣的型別不應該去用enumerate()去取索引和值,因為它會將dict的key作為元素值,並自己生成數值索引,也就是說dict的value被丟棄了。

>>> D
{`a`: 1, `b`: 3, `c`: 5, `d`: 7}

>>> list(enumerate(D))
[(0, `a`), (1, `b`), (2, `c`), (3, `d`)]

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)

相關文章