1. 程式人生 > >[Python]可變類型,默認參數與學弟的困惑

[Python]可變類型,默認參數與學弟的困惑

sim 不起作用 新的 roman 字符串 類型 元素 出了 div

一、學弟的困惑

十天前一個夜闌人靜、月明星稀的夜晚,我和我的朋友們正在學校東門的小餐館裏吃著方圓3裏內最美味的牛蛙,唱著最好聽的歌兒,暢聊人生的意義。突然,我的手機一震,氣氛瞬間就安靜下來,看著牛蛙碗裏三雙貪婪的筷子,我猶豫了:不——我的肉…但是本著不讓人久等的原則,我不舍地放下了筷子。點亮屏幕,我的眉頭不禁緊鎖,事情好像並不簡單…

技術分享圖片

什麽,還上升到了去醫院的程度?現在的年輕人怎麽了,怎麽那麽不註意安全,嗨,真是一屆不如一屆了,不過也好,沒受傷就好…正當我沈浸在我自己的瞎想時,一張圖片緊接著醫院那條發了過來…嗯?好熟悉的圖!

技術分享圖片

嗯…,這不是PyCharm嘛…原來是Python…啊不,我的牛蛙…當我還在想這會是個啥問題時,學弟發出了追問三連:

技術分享圖片

我是誰?我從哪裏來?我的牛蛙怎麽沒了?

右手無意思地點開了那張承載著學弟追問三連的圖,我倒要看看,什麽問題耽誤了我吃肉的最佳時機。

忽略學弟那莫名其妙的文件命名,以及那三位數的行數,學弟的問題由六行代碼引出:

  1. def li_si(a,ls=[]):
  2. ls.append(a)
  3. return ls
  4. print
    (li_si(7))
  5. print(li_si(15))
  6. print(li_si(45,[1,5,7]))
  7. print(li_si(78))

    一個函數,兩個參數,其中一個是默認的空列表,函數裏,列表對第一個參數執行append操作,返回列表。

    四個print(),每個print()的參數是一個函數調用,第一二四個函數調用只有一個參數,第二個參數使用的默認值。

    這會有啥問題?結果是顯而易見的嘛。

技術分享圖片

看來學弟進度有點慢啊。這麽基礎的知識,怎麽會扯上這麽多,什麽"局部變量",什麽"全局變量",還有"參數"之類,引得我嘴角上揚,感覺空氣中充滿了快活的空氣。

我夾起了一塊牛蛙肉,真香。

瞄了一眼程序的輸出結果,瞳孔瞬間放大。

技術分享圖片

不好,有詐!我仿佛聽到一聲驚雷,右手一抖,我的牛蛙掉到了大白菜湯裏,啊,牛蛙,你還是想回家啊。

哈哈,顧不得牛蛙了,看來學弟提了一個好問題,C語言裏那一套規則似乎不起作用了。

放下筷子,虔誠的拿起了可以打開未知世界大門的手機,思緒進入計算機世界,這幾行代碼在執行時,到底發生了什麽。

二、C語言裏的函數調用

當編譯器遇到一個函數調用時,它產生代碼傳遞參數並調用函數。C語言裏所有的參數均以"傳值調用"方式傳遞,而對於數組參數,傳遞的則是常量指針(數組)的拷貝。每次函數調用時,被調用的函數都有自己獨有的棧空間,裏面存儲了函數的參數、局部變量等信息,函數返回後,棧空間被釋放。

而Python的解釋器是用C寫的,Python裏的list底層就是C語言的可變數組,就是一個指針。

基於這種認知,我設想的運行結果應該是,第一二四個函數使用的默認參數list,每次調用時,默認參數都回有一個值,這個值是不確定的(後面會提到,在Python裏,可變類型竟然還真是確定的),所以每次調用時默認參數都(應該)指向空的數組,結果應該就是返回只有a一個元素的列表。

但是現在運行結果顯示,這三次函數調用時似乎指向了同一個列表,這就奇怪了。

三、我的猜想

本身應該是局部變量的參數,運行時卻有了全局變量的效果(我終於還是提到了學弟問的那幾個詞…),看著代碼,我有了這樣幾個猜測…

技術分享圖片

猜想1: 學弟這幾行代碼所在行數為106-112,有沒有可能在之前的代碼中,ls已經被定義過了,所以在後面的代碼中,全局的ls覆蓋了局部的ls,造成了這種參數全局的效果。

猜想2: 現在我也好奇當時我為嘛會想到這個…這解釋器怎麽可能會跨行優化這種…可能是被牛蛙沖昏了頭腦。

猜想3: 這個我做過實驗,對同一個函數多次調用,每次函數局部變量的地址都相同。所以我懷疑,默認參數所在內存區域的值,一直沒被修改,所以每次都一樣。不過這樣就有了一個悖論,第三次函數調用沒有使用默認的參數,內存區域的值理應被修改,但是第四次調用時又回到了前兩種情況。

四、放"碼"過來

回到學校後,終於有機會能實際跑跑這奇怪的代碼了,畢竟腦子不能編譯、解釋代碼,還是要上機。

首先,直接跑這7行代碼,看看結果。

技術分享圖片

嗯,和學弟的結果一樣,可以排除含有全局變量的情況1了。

看看每次函數調用時默認參數的值與地址。

技術分享圖片

這結果部分地驗證了猜想3,每次使用默認參數時都指向了同一個地址。

換一下,默認參數改為一個數字,這不會還指同一塊吧。

技術分享圖片

嗯…還指向同一塊,難不成這個默認參數的值放常量池了,怎麽老是指一個地兒…啊,對象,突然想起一句話,"Python裏萬物皆為對象",這麽想來,每一個數字都有自己單獨的地址了。嗯,實驗一下。

技術分享圖片

果然,都是對象。面向對象的特性爬出了書本,以這樣一種方式在我的面前刷了一波存在感。

因此,默認的參數ls,指向的也是同一個列表對象。而想要該變量指向新的列表的話,就得重新賦值。

技術分享圖片

重新賦值後,就得到了預期的結果。

五、可變類型與默認參數

Python的內建標準類型有一種分類標準是分為可變類型與不可變類型:

  • 可變類型:列表、字典
  • 不可變類型:數字、字符串、元組

變量保存的實際都是對象的引用,所以在給一個不可變類型(比如int)的變量a賦新值的時候,實際上是在內存中新建了一個對象,並講a指向這個對象,然後將原對象的引用計數-1。

所以當函數參數是默認列表時,它始終指向同一個對象,除非重新賦值,否則它並不會重新創建一個新列表。也就是說,多次調用函數執行append操作,實際上是對同一個對象進行操作。

參考:Python——可變類型與不可變類型(即為什麽函數默認參數要用元組而非列表)

python之函數默認參數及註意點

[Python]可變類型,默認參數與學弟的困惑