《從零開始PYTHON3》第六講
幾乎但凡接觸過一點編程的人都知道for循環,在大多數語言的學習中,這也是第一個要學習的循環模式。
但是在Python中,我們把for循環放到了while循環的后面。原因是,Python中的for循環已經完全不是你知道的樣子了。
for循環
以c語言為例,for循環幾乎是同while循環完全相同的功能。在Python中,for循環經過全新的設計,實際只支持一個功能,當然也是編程最常用到的功能,就是“遍歷”。
所謂遍歷(Traversal),是指沿着某條確定的搜索路線,依次對序列中的每個結點(每個元素)均做一次且僅做一次訪問。
比如最常見的字符串,實際就是一個序列。字符串“abcdefg”,中間包含7個字母,每個字母就是一個結點。“沿着某條確定的搜索路線”,其實指的就是按照何種規則來順序訪問字符串中的每個結點。最常見的可以使從開始到結尾,或者從結尾到開始。
我們先使用while循環來做一個字符串的遍歷:
s="abcdefg"
i = 0
while i < len(s):
print(s[i])
i += 1
在這個例子中,我們先定義了一個字符串,設定循環初始值0。while循環的邊界條件使用了內置標准函數len(),這個函數的功能是給出參數中包含的元素個數,在這里是字符的個數。
隨后在循環體中我們使用print函數在每次循環中打印出來一個結點(一個字符)。s[i]是s字符串中,第i個字符(結點)的意思,i在這里有一個專有名詞叫做“下標”,你可以相像數學中常用的$ S_i $形式。這種訪問某個序列中具體某個元素的方式是今天的重點之一。
這里i的取值范圍是從0開始,因此最大可以到字符串中字符總數-1。最后的i += 1,指的是按照從串頭到串尾的方式,循環訪問整個字符串中的所有字符。程序的執行結果是這個樣子:
a
b
c
d
e
f
g
補充一個小知識,剛才的循環中,我們使用了while i < len(s):,這可以工作的很好,理解起來也不難。但實際上,下面這樣做效率更高:
n=len(s)
while i < n:
...原因是,在前一個寫法中,len這個函數會執行很多次,循環每一次都要重新執行。而在后面的寫法中,len函數只需要執行一次。在其后的循環中,直接使用一個變量的值就要快多了。
遍歷是編程中最常用到的操作,也是最簡單的算法,希望你理解“遍歷”的含義了。
接下來我們看一看for循環來實現上面同樣的功能:
for a in "abcdefg":
print(a)
僅有兩行代碼,完成跟上面while循環程序完全相同的功能,簡潔了很多。執行結果跟上面完全一樣,就不再重貼了。為了便於理解,我使用偽代碼把for循環的基本形式重寫一遍:
for 遍歷變量 in 序列型的數據:
循環體,每次循環執行一遍,每次“遍歷變量”會有一個新值
這就是for循環的最基本形式。for/in/:是Python中的保留字。循環最終會執行的次數,等同於“序列型數據”中的元素個數。“遍歷”是對所有元素都要循環訪問一遍。
列表
for循環遍歷的對象必須是一個序列類型。序列類型並不是在Python中有一種特定的類型,而是一種統稱。可以理解為有順序、能順序訪問的類型都叫序列類型。列表類型是序列類型的一種。字符串類型也是序列類型的一種。
先看看數字的列表。這是一個數字列表的樣子:
[2,3,8.3,34,55,23]
使用中括號圈起來的,一組用逗號隔開的元素,就是列表。列表是Python六大數據類型中的一種,我們現在已經學習過了3種基本數據類型,數字、字符串、列表。這一講我們只是簡單引入列表的概念,來幫助我們理解“遍歷”,在第八講中,我們將正式而且更深入的講解列表這種數據類型。
跟字符串一樣,對數字的列表同樣可以使用len函數:
>>> len([2,3,8.3,34,55,23])
6
我們同樣可以使用for循環對數字列表進行遍歷,比如:
datas = [2,3,8.3,34,55,23];
for x in datas:
print(x)
#下面是執行的結果:
2
3
8.3
34
55
23
上面的數字列表中,我們混合了整數和浮點小數。從技術上講,列表中還可以同時包含“布爾”和“字符串”類型的數據。只是因為不同的數據類型,難以有共同的處理方式,放到同一個列表中也沒有辦法得到程序效率上的優勢,所以並不推薦那樣使用。
只要是列表的形式,就可以使用for循環來進行遍歷操作,從而提高處理速度。
我們再來對比遍歷數字列表的while循環模式和for循環模式:
#首先看while循環
i=0
while i<5:
print(i)
i += 1
#下面是for循環的方式
for i in [0,1,2,3,4]:
print(i)
#兩種循環的執行結果都是一樣的:
0
1
2
3
4
可以看到,for循環專門為了遍歷操作而生,在處理序列數據的時候,程序簡潔、代碼少、效率高。而while循環則有更強的通用性,但在處理遍歷任務的方面則略微麻煩。
為了讓for能夠處理更多通用的任務,Python提供了一個內置的標准函數range來自動生成一個序列,使用方法的偽代碼是:
#單參數方式,生成由0開始,到小於最大值的整數序列
range(最大值)
#雙參數方式,生成由最小值到最大值(不包含最大值本身)的整數序列
range(最小值,最大值)
#三參數模式,生成由最小值到最大值,以步長為遞增的序列
range(最小值,最大值,步長)
我們來看一組實際使用的例子來加深印象:
for i in range(5):
print(i)
#執行結果是:
0
1
2
3
4
for i in range(1,6):
print(i)
#執行結果是:
1
2
3
4
5
for i in range(1,10,2):
print(i)
1
3
5
7
9
range的注意事項是:range的參數、返回的序列都必須是整數。
我們前面見過了很多操作符,長得很不像關鍵字,比如+、-,今天我們終於看到了一個相反的例子,in操作符更像關鍵字,而不像操作符。當然操作符屬於關鍵字的一種。
除了在for循環中使用in操作符,in還可以用於邏輯判斷。比如:
"北京" in "今天下雨的地區有:北京、天津、河北" #結果是true
59 in range(60,101) #結果為:false
挑戰
我們今天的挑戰內容是編程生成斐波那契數列(Fibonacci sequence)。
斐波那契數列指的是這樣一個數列: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89...,這個數列從第3項開始,每一項都等於前兩項之和。
今天學習的主要內容是for循環,所以當然這個挑戰要使用for循環來完成,生成斐波那契數列的前100項。
老辦法,請大家先認真思考,用流程圖或者偽代碼描述自己的思路,覺得思路清晰了,再看下面的內容。
我們繼續使用快速原型法,首先是理清程序的需求,當做注釋內容寫入到程序:
"""
使用for循環生成前100項斐波那契數列
作者:Andrew
斐波那契數列指的是這樣一個數列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89...
這個數列從第3項開始,每一項都等於前兩項之和。
"""
接下來我們梳理在程序主體循環之前應當准備好的變量和初始值:
#以序列中任意連續3個數字來看
#a代表其中第一個數字,初始是1
a = 1
#b代表其中第二個數字,初始是1
b = 1
#c代表第三個數字,應當是a+b的和,但當前尚未進入循環,所以賦值為0
#因為python語言使用變量前無需聲明,所以實際上c=0可以省略
c = 0
#遍歷所用變量在for循環中定義,這里忽略
跟上一講的例子不同,斐波那契數列肯定是邊生成邊輸出,所以肯定是要在循環之內來完成輸出的工作。所以不像上一講的例子,可以先確定輸出的內容。
直接進入到考慮循環體的環節,首先依然是循環的邊界:
#從第3項開始,循環到第101項
for i in range(3,101):
循環到101項的意思是因為,前面講過了,range函數所產生的序列,不包含給定的最大值本身,所以range(3,101)實際會產生從3、4、5到100的序列。
參考前面的內容,我們把主體部分的內容一起列出來:
#前兩項不用計算,直接顯示
print("第 1 項為:",a)
print("第 2 項為:",b)
#從第3項開始,循環到第101項
for i in range(3,101):
c = a + b #計算第三項
a = b #3個元素的窗口向后移,原第一個元素被拋棄
b = c #第二個元素更新為新計算的項
print("第",i,"項為:",c) #顯示
為了看起來更清楚,我們這次使用截圖來展示上面程序的輸出結果:
程序優化
有人說“好文章是改出來的。”,其實好的程序也是一樣。程序編寫第一項任務是完成需求所定義的基本工作。隨后就要根據程序的表現,有針對性的優化。程序優化最基本的任務通常是速度和內存的占用。因為我們目前的學習還比較基礎,暫時不會涉及到那些部分,所以我們先對程序的結構和代碼量進行優化。目標是結構更清晰易讀,代碼更精簡高效。
首先我們要根據當前程序的情況進行分析評估,根據評估的結果決定下一步的改進方向。以當前的程序情況來說,可以容易的發現以下幾項問題:
- 斐波那契數列生成的過程中,前兩項的生成是單獨處理的,跟后面的98項不統一,這會造成將來對程序修改、重用的時候,這兩項都要單獨處理,維護性差。
- 也因為對頭兩項單獨的處理,多次使用了print函數,造成代碼冗余。
- 變量c在顯示完成后實際可以不用保存,沒有必要使用,這造成內存的浪費。
- 最后是沒有進行函數化,可重用性差。
根據我們的分析結果,進行程序優化之前,我們補充一點知識。在第四講的做練習的時候,為了求甲、乙雙方的速度,我們曾經自定義一個函數,最后求得結果的時候是這樣一句:
#計算原題:當甲乙雙方相距36千米時雙方的速度
x,y = getSpeed(36) #getSpeed函數,最后使用了return x,y
這種使用方法很自然,跟單獨一個變量的賦值比起來,效率也更高。我們在這里總結一下為變量賦值的幾種形式:
#常規的賦值
a=1
c="abcd"
#多元賦值
x,y = 2,3
x,y = 3,2
x,y = y,x #注意不是數學等式,這是交換兩個變量的值
x,y = y,x+b
#連續賦值
a=b=c=d=10 #賦值結束后,變量a/b/c/d都將是10
好了,我們對程序進行優化。剛才講到的多元賦值也能用來優化這個程序:
"""
使用for循環生成前100項斐波那契數列
作者:Andrew
斐波那契數列指的是這樣一個數列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89...
這個數列從第3項開始,每一項都等於前兩項之和。
"""
a = 1
b = 1
print("第 1 項為:",a)
for i in range(2,101):
print("第",i,"項為:",b) #顯示
a,b = b,a+b #采用多元賦值,直接完成下一項計算和窗口的后移
不錯吧?怎么看都能感覺到清晰的進步。然而,兩個存在的問題依然沒有解決:
- 隊列中第一項數字仍然單獨處理;
- 仍然沒有函數化。
函數化其實比較簡單,把第一項數字也納入整體生成的考慮就需要算法的調整。這個過程一般只能進行數學上的分析和經驗的積累。所以這里我直接說答案:
在第一版的時候,我們使用了3個數字的“窗口”,因為第三個數字是前兩個數字之和。
第二版的優化,我們知道了第三個數字其實可以省略不保存,所以只使用了2個數字的“窗口”,因為隊列中第三個數字需要前兩個數字之和,所以這2個數字的窗口,實際無法繼續省略。
既然無法省略,並且要保持2個數字的窗口。我們把數字向前延伸一位,增加一個第0項,值是0,並且無需顯示,這個問題就簡單了,直接看源碼:
#我們省略了開始的注釋
def fibonacci(n):
#為斐波那契數列之前添加一個不顯示的第0項:0
#0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89...
#以序列中任意連續2個數字來看
#a代表其中第一個數字,初始是0
#b代表其中第二個數字,初始是1
a,b = 0,1 #使用連續賦值簡化代碼
#從第1項開始,循環到第n項,結尾邊界為n+1
for i in range(1,n+1):
print("第",i,"項為:",b) #顯示
a,b = b,a+b #采用多元賦值,直接完成下一項計算和窗口的后移
#調用函數,生成前100項
fibonacci(100)
以我們課程涉及的范疇看,當前基本算最優的算法了,優化至此結束。
練習時間
請用戶輸入一個整數n,使用for循環的方法,求整數1、2、3一直到n(包含n本身)的和。
本講小結
- 本講講述了for循環在遍歷操作中的應用以及跟while循環的對比
- 遍歷是計算機重要的一種操作模式,會經常用到,從而也讓for循環成為最常用的循環模式
- 運算符也是關鍵字,關鍵字、語法、函數組成了語言學習的主要部分
- 算法方面:一個復雜的問題要逐步拆解,從微小處開始優化。如果還感覺難,就把問題想辦法再拆的小一點
- 其實語言很好學,但一門語言是死的,配合上算法才能完成具體的工作
- 多元賦值是python的特色,也是優點,能在很多場合簡化代碼、清晰結構
參考答案
練習題參考答案請參考源碼ex.py。