Python 中那些令人防不勝防的坑(二)

在這裏插入圖片描述


大家好,我是 Rocky0429,一個正在學習 Python 的蒟蒻…


人不能兩次踏入同一條河流,在無數次踩進同樣的坑裏之後,我覺得我有必要整理一下,這是 Python 防坑系列第二篇。


如果你還沒讀過第一篇,請點擊下面鏈接:


Python 中那些令人防不勝防的坑(一)


這會是一個系列,每篇 5 個,系列文章更新不定,不想錯過的,記得點個關注,不迷路。



0x00 嫌棄的默認可變參數


首先我們先來看一個例子:


def test_func(default_arg=[]):
   default_arg.append('rocky0429')
   return default_arg

我們都知道如果調用上述函數 1 次以後所出現的結果:


>>> test_func()
['rocky0429']

那麼如果調用 2 次,3 次呢?你可以先自己思考一下再繼續看下面的結果:


>>> test_func()
['rocky0429', 'rocky0429']
>>> test_func()
['rocky0429', 'rocky0429', 'rocky0429']

咦?明明我們的函數裏明明對默認的可變參數賦值了,爲什麼第 1 次調用是初始化的狀態,第 2 次,第 3 次出現的結果就不是我們想要的了呢?先別急,我們再繼續看下面的調用:


>>> test_func([])
['rocky0429']
>>> test_func()
['rocky0429', 'rocky0429', 'rocky0429', 'rocky0429']

是不是更懵了?


其實出現這樣的結果是因爲 Python 中函數的默認可變參數並不是每次調用該函數時都會初始化。相反,它們會使用最近分配的值作爲默認值。在上述的 test_func([]) 的結果不同是因爲,當我們將明確的 [] 作爲參數傳遞給 test_func() 的時候,就不會使用 test_func 的默認值,所以函數返回的是我們期望的值。


在自定義函數的特殊屬性中,有個「 defaults」 會以元組的形式返回函數的默認參數。下面我們就用「 defaults」來演示一下,以便讓大家有個更直觀的感覺:


>>> test_func.__defaults__ #還未調用
([],)
>>> test_func() # 第 1 次
['rocky0429']
>>> test_func.__defaults__ # 第 2 次的默認值
(['rocky0429'],)
>>> test_func() # 第 2 次
['rocky0429', 'rocky0429']
>>> test_func.__defaults__ # 第 2 次的默認值
(['rocky0429', 'rocky0429'],)
>>> test_func([]) # 輸入確定的 []
['rocky0429']
>>> test_func.__defaults__ # 此時的默認值
(['rocky0429', 'rocky0429'],)

那麼上面那種情況該如何避免呢?畢竟我們還是希望在每次調用函數的時候都是初始化的狀態的?這個也很簡單,就是將 None 指定爲參數的默認值,然後檢查是否有值傳給對應的參數。所以對於文章開始的那個例子,我們可以改成如下的形式:


def test_func(default_arg=None):
   if not default_arg:
       default_arg = []
   default_arg.append('rocky0429')
   return default_arg


0x01 不一樣的賦值語句


首先我們先來看一行代碼:


a, b = a[b] = {}, 5

看完上面的代碼,現在問題來了,你知道 a,b 的值是多少麼?先仔細思考一下。如果思考完畢,請繼續往下看。


在交互模式中輸出一下,結果如下所示:


>>> a
{5: ({...}, 5)}
>>> b
5

怎麼樣?猜對了麼?我猜大多數人看到這個結果都會很懵圈,就算不說結果,很多人看到最開始的那行代碼,也會覺得沒有頭腦,下面就讓我來詳細的說一下,爲什麼是這樣。


首先關於賦值語句,很多人都用過,但是更多的只是常用的形式,就是 a = b 這種模式,很少有人去看官方文檔中關於賦值語句的形式:


(target_list "=")+ (expression_list | yield_expression)

上面的 expression_list 是賦值語句計算表達式列表,這個可以是單個表達式或者是以逗號分割的列表(如果是後者的話,返回的是元組),並且將單個結果對象從左到右分給目標列表(target_list)中的每一項。


下面我結合這個賦值語句的形式和文章開頭的代碼詳細說一下爲什麼會出現這樣一個我們猜不到的結果:


首先是 (target_list “=”)+,前面好容易理解,後面帶着的 + 意味着可以有一個或者多個的目標列表。在上面的代碼中,目標列表就有兩個:a, b 和 a[b]。這裏要注意的是「表達式列表」只能有一個({}, 5)。


表達式列表計算結束後,將它的值從左到右分配給目標列表。在上面的代碼中,即將 {},5 元組並賦值給 a, b,所以我們就得到了 a = {},b = 5(此處 a 被賦值的 {} 是可變對象)。


接着我們來看第二個目標列表 a[b],很多人對這個地方有困惑,覺得這個地方應該報錯,因爲他們覺得在之前的語句中 a 和 b 並沒有被賦值。其實我們已經賦值了,我們剛將 a 賦值了 {},b 賦值了 5。


下面我們將 a 字典中 5 鍵的值設置爲元組 ({}, 5)來創建循環引用,{…} 指的是與 a 引用了相同的對象。


下面再來看一個簡單一些的循環引用的例子:


>>> test_list = test_list[0] = [0]
>>> test_list
[[...]]
>>> test_list[0]
[[...]]
>>> test_list[0][0][0][0] is test_list
True

其實在文章最初時的那行代碼中也是像這樣的,比如 a[b][0] 和 a 其實是相同的對象,同樣 a[b][0][b][0],a[b][0][b][0][b][0],… 都和 a 是相同的對象。


>>> a[b][0][b][0] is a
True
>>> a[b][0] is a
True

如上,我們也可以完全把文章開頭的例子拆解成如下形式:


a, b = {}, 5
a[b] = a, b

這樣,是不是更好理解一些了呢?



0x02 捕獲異常不要太貪心


使用 Python 可以選擇捕獲哪些異常,在這裏必須要注意的是不要涵蓋的範圍太廣,即要儘量避免 except 後面爲空,最好是要帶東西的。except 後面如果什麼也不帶,它會捕捉 try 代碼塊中代碼執行時所出現的每個異常。


雖然後面什麼也不帶在大多數情況下得到的也是我們想要的結果,但是代碼塊中如果是個嵌套結構的話,它可能會破壞嵌套結構中的 try 得到它想要的結果。比如下面這種情況:


def func():
try:
# do something1
except:
# do something2

try:
func()
except NameError:
# do something3

比如上面的代碼,如果在 something1 處出現了 NameError,那麼所有的異常都會被 something2 處捕獲到,程序就此停掉,而正常情況下應該捕獲到 NameError 的 something3 處則什麼異常也沒有。


上面只是說了一個簡單的情況,因爲 Python 運行在個人電腦中,可能有時候內存錯誤,系統莫名退出這種異常也會被捕捉到,而現實情況是這些和我們當前的運行的程序一毛錢關係也沒有。


可能這時候有人會想到 Exception 這個內置異常類,但實際情況是 except Exception 比 except 後面什麼也不帶好不到哪裏去,大概也只是好在系統退出這種異常 Exception 不會捕捉。


那該如何使用 except 呢?


那就是儘量讓 except 後面具體化,例如上面代碼中的 except NameError: ,意圖明確,不會攔截無關的事件。雖然只寫一個 except 很方便,但有時候追求方便恰恰就是產生麻煩的源頭。



0x03 循環對象


循環對象就是一個複合對象包含指向自身的引用。無論何時何地 Python 對象中檢測到了循環,都會打印成 […] 的形式,而不是陷入無限循環的境地。我們還是先看一個例子:


>>> lst = ['Rocky']
>>> lst.append(lst)
>>> lst
['Rocky', [...]]

我們除了要知道上面的 […] 代表對象中帶有循環之外,還有一種容易造成誤會的情況也該知道:「循環結構可能會導致程序代碼陷入到無法預期的循環當中」。


至於這句話我們現在不去細究,你需要知道的是除非你真的需要,否則不要使用循環引用,我相信你肯定不想讓自己陷入某些“玄學“的麻煩中。



0x04 列表重複


列表重複表面上看起來就是自己多次加上自己。這是事實,但是當列表被嵌套的時候產生的效果就不見得是我們想的那樣。我們來看下面這個例子:


>>> lst = [1,2,3]
>>> l1 = lst * 3
>>> l2 = [lst] * 3
>>> l1
[1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> l2
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]

上面 l1 賦值給重複四次的 lst,l2 賦值給包含重複四次 lst的。由於 lst 在 l2 的那行代碼中是嵌套的,返回賦值爲 lst 的原始列表,所以會出現在「賦值生成引用」這一節中出現的那種問題:


>>> lst[0] = 0
>>> l1
[1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> l2
[[0, 2, 3], [0, 2, 3], [0, 2, 3]]

解決上面問題和之前我們說過的一樣,比如用切片的方法形成一個新的無共享的對象,因爲這個的確是以另一種生成共享可變對象的方法。


另外本蒟蒻把公衆號的高分原創文章整理成了一本電子書,取名《Python修煉之道》,一共 400 頁!

具體內容請戳:熬夜爆肝整理 400 頁 《Python 修煉之道》,一本高分原創高清電子書送給你!

目錄如下:


在這裏插入圖片描述

現在免費送給大家,在我的公衆號Python空間(微信搜 Devtogether) 回覆 修煉之道即可獲取。



作者Info:

【作者】:Rocky0429
【原創公衆號】:Python空間。
【簡介】:CSDN 博客專家, 985 計算機在讀研究生,ACM 退役狗 & 亞洲區域賽銀獎划水選手。這是一個堅持原創的技術公衆號,每天堅持推送各種 Python 基礎/進階文章,數據分析,爬蟲實戰,數據結構與算法,不定期分享各類資源。
【福利】:送你新人大禮包一份,關注微信公衆號,後臺回覆:“CSDN” 即可獲取!
【轉載說明】:轉載請說明出處,謝謝合作!~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章