路遙知碼力,日久練成精- 只要在程式之路鑽研的夠深,便能夠充分發揮程式碼的力量; 練習的日子夠久,便能夠練成寫出精簡代碼的能力。
本篇要來談談一些python看似正常的語法卻令人意外的結果。
照慣例我們先討論一下昨天課後學習的解答,
(還沒看過題目的朋友歡迎點昨日題目傳送門)
我覺得本段談的東西並不那麼常用,
如果這一段看不懂也沒關係,
等等覺得難以理解的話可直接略過此段看本文。
昨日問題是猜以下程式碼的執行結果:
mulFuncs = [lambda x: x*i for i in range(1,6)]
for func in mulFuncs:
print(func(2))
直覺來說,列表生成式內的匿名函數計算的不就是x*1
, x*2
,..., x*5
的值嗎?
因此你的預期結果應該是2
4
6
8
10
吧?
但實際執行程式後,你卻會得到5個10
:10
10
10
10
10
驚訝不驚訝?
這就要從python中的變數查找規則LEGB(local, enclosed, global, bulid-in)說起了,
至於具體來說LEGB是什麼東西,
你可以參考namespace與LEGB scope規則這篇文章,
本文不詳加探討。
python使用變數時,會依照local, enclosed, global, bulid-in的順序找起,
在lambda x: x*i
這個函數中,i
並沒有做為函數參數傳進函數中,i
並非一個local變數,只能往外在enclosed區域中找起,
for迴圈跑完時,i
的值變成了5,
於是每個lambda x: x*i
參考的值也成了i=5
的值。
那如何修正呢?
在宣告變數時,使i
成為一個loacl變數傳進函數就行,
如下:
mulFuncs = [lambda x, m=i: x*m for i in range(1,6)]
for func in mulFuncs:
print(func(2))
(上述這段蠻進階的,若對你來說很困難直接略過沒關係的。)
那麼python中還有哪些隱藏的坑來其中呢?
在python裡面最常碰到的坑,
我覺得以可變變數list應該算榜上有名,
因為它可變的特性,有時候你在不經意的時候改到它的值,
導致陷入萬劫不復的深淵,
例如我們很久以前於Day3舉過的冰箱的的例子即為一例。
注意我這邊講的「坑」,
指的是程式碼看起來沒問題,但執行結果卻跟你想的不一樣,
如果是一些新手常碰到的問題:
如「縮排tab和空格混著用」、「忘記加冒號」,
我覺得都不算是真正的「坑」,
因為光執行程式都無法編繹過,
自然會知道程式碼是有錯的。
究竟python的坑常在哪裡出現,
又該如何避免踩坑,
以免寫出一個要花好幾個小時來找的程式錯誤呢?
以下一一介紹寫python應注意的陷阱。
這個例子我們在Day3中已經講過,
這邊再次整理給大家:
<錯誤>
fridge = ['蛋糕', '蘋果', '香蕉']
b = fridge
b.remove('蛋糕')
print("列表b的內容為:", b)
print("列表fridge的內容為:", fridge)
結果為:列表b的內容為: ['蘋果', '香蕉']
列表fridge的內容為: ['蘋果', '香蕉']
你希望b
和fridge
是互不影響的變數,
修改了b
的內容卻跟著影響了fridge
的內容。
<修正>
將第二行程式改為:b = fridge[:]
或者b = fridge.copy()
假設你寫了一個函數,
希望將新的物件添加到列表中,
默認參數為空列表,
如下:
def add_to_list(obj, List=[]):
List +=[obj]
return List
看起來沒什麼問題。
測試以下執行結果:
print(add_to_list(5))
print(add_to_list(4))
print(add_to_list(3))
你心目中預期的結果應該會是[5]
[4]
[3]
但實際出現的結果卻是[5]
[5, 4]
[5, 4, 3]
驚喜不驚喜?
這意味著每次調用函數用的都是同一個list,
也就是函數創好的時候,
默認參數只會被產生一次,
而非你心目中想像的每呼叫一次函數變重新產生一次參數。
要避免這種陷阱,
最好用不可變變數做為參數:
建議解法:
def add_to_list(obj, List=None):
if not List:
List=[]
List +=[obj]
return List
我們都知道如何產生一個初始值為0,
長度為7的一維list,如下:
List= [0]*7
print(List)
結果為:[0, 0, 0, 0, 0, 0, 0]
,
看起來蠻好的。
那要如何產生一個初始值為0,
大小為3*3的二維list呢?
相信很多人會舉一反三,寫出以下的程式碼:
List_2D = [[0]*3]*3
print(List_2D)
結果為:[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
乍看之下也沒什麼問題。
但是當我們嘗試修改list內的值時:
List_2D = [[0]*3]*3
List_2D[0][1] = 10
print(List_2D)
結果為:[[0, 10, 0], [0, 10, 0], [0, 10, 0]]
咦?每一列的第一個元素竟然都被改掉了?
追根究柢原因為其實這三個list都貼上同一張標籤。
正確寫法為採用列表生成式以生成三個不同物件:
List_2D = [[0]*3 for i in range(3)]
List_2D[0][1] = 10
print(List_2D)
現在已經能正確顯示結果了: [[0, 10, 0], [0, 0, 0], [0, 0, 0]]
enumerate
可以很方便的訪問列表,
我們來看看正常的用法:
A= ['H', 'e', 'l', 'l', 'o']
for i,e in enumerate(A):
print(i,e)
結果為:0 H
1 e
2 l
3 l
4 o
enumerate
透過for加兩個變數迭代,
可以同時取得列表的index及內容,非常方便。
但如果我們嘗試邊訪問列表邊修改它的話,
可能就會發生意料之外的錯誤,例如:
A= ['H', 'e', 'X', 'Y', 'l','l','o']
for i,e in enumerate(A):
if e=='X' or e=='Y':
del A[i]
print(A)
在此例中,我們希望把列表中的'X'
字元及'Y'
刪除,
但印出來的結果是:['H', 'e', 'Y', 'l', 'l', 'o']
咦?只有刪掉一個耶?怎麼回事?
我們把迭代過程中的i,e
印出來會明白一些:
A= ['H', 'e', 'X', 'Y', 'l','l','o']
for i,e in enumerate(A):
print(i,e)
if e=='X' or e=='Y':
del A[i]
結果為:0 H
1 e
2 X
3 l
4 l
5 o
哦哦,原來如此,index在遞增,但列表變短了,
當'X'
這個字被刪除後,本來在index「3」位置的'Y'
字元就跑到「2」這個位置了,
但是for迴圈繼續往下走,故沒有看到'Y'
這個字呢。
<修正>:
當邏輯簡單時,可以直接用列表生成式來解,
例如:
A= ['H', 'e', 'X', 'Y', 'l','l','o']
A= [c for c in A if c!='X' and c!='Y']
print(A)
結果為:['H', 'e', 'l', 'l', 'o']
在某些初級教程中,你可能會看到說x+=y
就是x=x+y
的簡潔寫法,
嗯…對初學者來說是可以暫時把它們想成是相同的比較單純,
但事實上,對可變變數來說,這兩者之間還是有些微妙的差異,
請看例子:
x = [1,2,3]
z = x
x += [4]
print("列表x為", x)
print(z)
第一支程式創建了一個列表,名字叫做x
,
這時在拿一張名字為z
的標籤貼上去,
因此x
修改時,z
也跟著受影響。
結果為列表x為 [1, 2, 3, 4]
[1, 2, 3, 4]
。
我們再看一支很像的程式,
只是把x += [4]
改成x = x+[4]
:
x = [1,2,3]
z = x
x = x+[4]
print("列表x為", x)
print(z)
結果為列表x為 [1, 2, 3, 4]
[1, 2, 3]
。
此時列表z
不受影響了耶,
可以想成是用=
賦值的時候真的創建了一個新的物件,
而使用+=
的時候則是原地修改x
這個列表,
故使用+=
時會跟著影響z
。
以上便是python語法中比較常見的一些陷阱了,
今日無課後練習,
我們明天再見囉。
您好,想請問我的想法哪邊不對呢?
def add_to_list(obj, List=None):
if not List:
List=[]
List +=[obj]
return List
print(add_to_list(3))
>>> [3]
以上程式碼中,
print(add_to_list(3)) 沒有輸入 List 參數,
所以 List == None ,if 條件應該不會執行,
也就是 List 仍然是 None ,而不是 [] ,
List+[obj] 應該要報錯才對,但實際上會得到 [3] ,
請問我哪裡想錯了呢?
我自己想到了, 因為 not List 為 True ,所以默認參數下 if 都會執行。
剛剛不知道為甚麼一直想不到 XD