python调用函数传参时、有默认值的在中间 报错了_python的大坑:使用空列表作为默认参数,让我怀疑遇到了灵异代码...
在python中,不要使用列表或者其他可變類型的數據容器作為默認參數。否則你很可能會遇到奇奇怪怪的問題。
如果你在調用某一個函數時,傳了同樣的參數,手動執行,每次結果都正確。但是用循環遍歷重復多次執行,每次得到的結果都不一樣,并且每執行一次,它返回的數據都是在上一次的基礎上繼續加多一次。那么恭喜你,很可能遇到了跟我一樣的問題。
一、問題背景
最近在用django框架開發一個web應用,專門寫了個函數從某個接口調取數據,對數據進行處理后,返回給前端頁面。
但是出現了奇怪的問題。當我直接在pycharm上單獨執行這個函數所在的模塊或者單獨調用這個函數時,它執行后,返回的數據是沒問題的,而且就算在短時間內,重復run多次,它的結果都沒問題,都是一樣的。
但是,當我把它放在django項目中,整個項目一起運行,在前端頁面點擊發起請求,后端調用這個函數時,第一次是沒問題的,但如果很短時間內再次發起同樣的請求,獲得的數據會疊加上一次的數據一起返回。而如果是隔一段時間之后再發起同樣的請求的話,它第一次又沒問題了,而第二次執行又有問題了。
這個讓我懵圈了好幾天,以為遇到了靈異代碼了。想破腦袋都搞不明白到底問題出在哪里。中間甚至懷疑過是不是前端ajax在一次請求下,多次調用了那個函數。也懷疑過是不是接口服務器的響應機制有問題。或者是我寫的程序邏輯上有問題。但通過一步步排查,這些都被否定了,全都沒問題。
這時候是很麻煩了,當你知道問題出在哪里時,查一下也許就有答案了,如果連問題出在哪里都看不出,那就只能一點一點排查了。
在排查的過程中,我一步一步縮小范圍,最終把“病根”鎖定到這個函數的參數上。根本原因就是:我在函數的參數上,使用了一個列表作為默認參數。
這個做法如果脫離具體應用場景來講,它在邏輯上是沒問題的,python解釋器允許這么做,執行了也不會報錯。但關鍵就是python這門語言的內存引用機制導致了2種情況的出現:如果你短時間內手動重復執行它多次,不會有問題;但如果在一次執行中,使用循環去重復調用,就可能會有異常情況出現。
那么到底為什么使用空列表作為默認參數會有問題呢?
準確來說應該是:使用所有可變類型的數據容器(例如列表、字典)作為默認參數,都有可能出現這個問題。
二、模擬重現這個問題:
原項目中整個函數的代碼太長了,為了凸顯重點問題所在,我把原來整個函數的內容縮減,重新寫了2個示例函數來進行對比:
1、簡單說說這2個函數的作用和區別:
2個函數的作用都一樣:
都是傳進去一個店名store_name,判斷頁碼page_num是不是第2頁。如果不是,就把數據添加到page_data這個列表中,把頁碼加1,重新再執行一遍這個函數。如果page_num是第2頁的話,就把整個處理后的列表返回。
2個函數的區別:一個直接使用空列表作為默認參數,一個先把空列表賦值給變量,再將這個變量作為位置參數傳遞給函數;
2、單次運行中單次調用它們會怎么樣?
我們來直接運行這個模塊,單次調用這2個函數,給它傳一個店名“肯德基”,執行看看效果是怎么樣的。
(注意:第一個函數它有個默認的空列表參數了,不用我們傳參,它也能默認page_data是個空列表來執行,而要調用第二個函數,必須先定義一個空列表,把它作為參數傳進去,這樣效果看起來就是一樣的)
2個函數都只調用一次,調用代碼如下:
按照正常的思維,我們可能會覺得,2個函數的執行結果是一樣的。
那么我們看看輸出的結果:
沒問題,輸出結果確實也是一樣的。證明單次執行沒有問題。手動多次重復執行,得到的結果也是沒問題的,每次都一樣。
3、單次運行中多次調用這個函數會怎么樣?
用for循環將2個函數都各調用2次,代碼如下:
結果如下:
這個結果是不正常的。
當空列表作為默認參數時:
在第一次for循環中,get_data_1函數內部循環執行了2次,得到了一個列表[ ‘肯德基’, ’ 肯德基 ’ ];
而第二次for循環剛開始,它就把上一次for循環得到的列表,作為第二次for循環的默認參數繼續使用,并在函數內部執行了2次添加,所以第2次for循環執行的結果是[ ‘肯德基’, ’ 肯德基 ',‘肯德基’, ’ 肯德基 ’ ]。
然而,我們在第二次for循環調用它之前, 并沒有給它的page_data賦任何值啊,是希望它使用默認參數來執行的,那就奇怪了,沒賦值為什么它卻有值?而且還是使用上一次for循環得到的數據。
而當空列表作為位置參數的時候:
即使短時間內執行2次,它的2次輸出結果還是一樣的。這是符合我們預期的。
正常來說,如果我們不打算對一個函數的調用次數作限制,那么當我們在相同參數的條件下重復調用這個函數,得到的結果就必須是一樣的,不能因為調用次數多了,輸出結果就不一致。所以很明顯,get_data_1這個函數是有問題的,而get_data_2這個函數是沒問題的。
那么是什么問題導致了這種情況?
get_data_1和get_data_2的不同之處只是get_data_1使用了空列表作為默認參數。所以通過繼續研究,我最后得出的結論是:這個問題是python這門語言的特性導致的。
三、分析背后隱藏的問題
這里必須先說4個相關的問題,就是這4個問題同時出現導致了在項目下運行會出現異常,而單獨在模塊中手動重復調用沒問題,循環重復調用會有問題。
1、為一個變量賦值的過程中,python解釋器做了什么事?
當我們為一個變量賦值時,python解釋器會先在計算機內存中,開辟一個內存空間,用于存放這個變量的值。當你為另一個變量賦值時,如果要賦的值跟第一個值是一樣的,那么python解釋器不會重新再開辟一個內存空間給你第二個變量使用,它只會調整你第二個變量的指向,讓它指向第一個值存儲的內存空間,你每次調用第二個變量時,它會直接到第一個值存儲的內存空間去拿出數據。
演示一下:
打開ipython,在交互環境下,分別給a和b賦值,通過id(a)和id(b)可以獲取到它們的內存地址。
我們可以看到,當你給a和b賦值同樣的內容時,它們的內存地址都是一樣的。證實了上面所說的這個python語言特性。
2、當對一個列表執行append()操作時,改變了它的值,會不會改變它的內存地址?
答案是:不會。
這個也可以演示一下:
當我們定義了一個空列表之后,即使你使用append(),給列表追加了內容,讓它的值不一樣,但執行前后2個變量的id還是不變的。
這說明了什么?在改變一個可變類型的數據時,python解釋器只是將原來在內存中存放的值給你修改了,而不是重新給你一個內存空間去存放新的值。
3、python解釋器什么時候釋放內存?
當我們在一個模塊下執行一整片代碼時,python解釋器是會從頭到尾執行的,在執行結束之前,所有變量的賦值都會保持在內存中,隨時可以調用。只有整個模塊運行結束,內存才會被釋放,所有變量的賦值會從內存中釋放出來。
4、Django項目運行下,python的變量賦值是否會一直保存在內存中。
分為2種情況:
debug模式下(debug=Ture),django會加載reloader,隔一段時間重啟一次服務,在每次服務重啟之前,整個項目的變量賦值都會保存在內存中,只有重啟服務才會釋放內存后重新分配內存空間。
生產模式下(debug=False),不會自動重啟服務,所以變量賦值會一直保存在內存空間中。
搞明白這4個問題之后,我們就可以重新分析一下,使用空列表作為默認參數為什么會產生這個問題。
四、我們來分析一下整個調用的過程中,python解釋器都做了什么事:
分析細節請看圖片上的文字
為了證實我的分析是正確的,我們可以分別在get_data_1和get_data_2之后,對page_data的內存地址進行打印,看看輸出的結果。
輸出結果:
結果正如所料:
第一次for循環中,get_data_1執行了2次,而輸出的page_data內存地址始終沒變化,說明python解釋器始終調用的是同一個內存地址存放的值,即使它的值在第一次被調用的時候已經改變,也不影響它再次調用。
第二次for循環中,輸出的page_data內存地址2次都不一樣,說明python解釋器在循環2次調用get_data_2之前,都有對變量page_data進行初始化重新賦值,所以不管你前一次對page_data的值做了什么改變,它最終都是先從空列表開始引用。
所以結論就是:python的內存引用機制,導致了當我們使用了可變類型的數據容器作為默認參數后,你的程序可能在某些操作下改變了它的值,而這個操作剛好是不會改變它的內存地址的,這時候python解釋器只會在內存中對這個值進行修改,但暫時不會把它釋放出來,所以你再一次引用這個默認參數時,會以為它還是你設置的默認參數值(空列表),但實際上它已經被修改過了。
五、如何避免這個問題:
1、不要使用可變類型的數據容器作為默認參數(包括列表,字典),但是可以作為位置參數使用。
你可以使用get_data_2這種寫法,把列表或可變數據類型的數據容器,在每次調用之前都進行賦值,然后使用位置參數傳參;
2、如果你一定要使用,需要在函數內部進行判斷,判斷它是不是你想要的初始值,如果不是,就再次進行賦值。
但這種方法只適用于部分情況,當你的函數本身會循環執行,而且你在外部需要循環調用時,你是無法確定它到底是處在外部循環中還是處在內部循環中的,所以根本無法對它每一次的值進行判斷。
例如我上面這個get_data_1函數就不適合這么改,因為它是會根據page_num的值,來決定本身的循環次數,并且自動對參數page_data的值進行調整。所以我無法判斷當我在外部進行循環調用的時候,到底它是處在我外部的循環中,還是處在它函數內部的循環中,因此無法確定參數的值應該是多少。
最后感慨一下:感覺遇到了我這個段位不應該遇到的問題。
人生苦短,我學python,學了python,人生更短。
總結
以上是生活随笔為你收集整理的python调用函数传参时、有默认值的在中间 报错了_python的大坑:使用空列表作为默认参数,让我怀疑遇到了灵异代码...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: u盘怎么无法打开了 U盘无法正常打开,如
- 下一篇: perl语言入门第七版中文_python