Python循環結構用法

本文介紹python中的while循環、for循環。在python中for能夠用於循環,也可用於另外一種近親的列表解析,列表解析是python中很是重要的特性,詳細內容見後面的文章。html

通常來講,python寫for循環比寫while更容易、方便,並且python中的for比while效率要更高,若是能夠,用for而不是while。python

while循環

python中的while/for循環和其它語言的while循環有些不同,它支持else分支。結構以下:bash

while <CONDITION>:
    CODE
else:
    CODE_ELSE

注意,condition部分只能是表達式,不能是語句,因此condition中不能包含賦值語句,如while a = x:是錯誤的。app

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,則邏輯更清晰:code

while x:
    if match(x[0]):
        print("found it")
        break
    x = x[1:]
else:
    print("not found")

再例如,判斷一個數(以下面的y)是不是質數。htm

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中。blog

  • 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":
        del D[i]
        print(i)

print(D)

報錯:

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)
相關文章
相關標籤/搜索