本文將介紹使用mutable對象做爲Python函數參數默認值潛在的危害,以及其實現原理和設計目的html
http://cenalulu.github.io/python
http://cenalulu.github.io/python/default-mutable-arguments/git
咱們就用實際的舉例來演示咱們今天所要討論的主要內容。
下面一段代碼定義了一個名爲generate_new_list_with
的函數。該函數的本意是在每次調用時都新建一個包含有給定element
值的list。而實際運行結果以下:github
{% highlight python %}
{% raw %}
Python 2.7.9 (default, Dec 19 2014, 06:05:48)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
Type "help", "copyright", "credits" or "license" for more information.編程
def generate_new_list_with(my_list=[], element=None):
... my_list.append(element)
... return my_list
...
list_1 = generate_new_list_with(element=1)
list_1
[1]
list_2 = generate_new_list_with(element=2)
list_2
[1, 2]app{% endraw %}
{% endhighlight %}編程語言
可見代碼運行結果並不和咱們預期的同樣。list_2
在函數的第二次調用時並無獲得一個新的list並填入2,而是在第一次調用結果的基礎上append了一個2。爲何會發生這樣在其餘編程語言中簡直就是設計bug同樣的問題呢?ide
要了解這個問題的緣由咱們先須要一個準備知識,那就是:Python變量究竟是如何實現的?
Python變量區別於其餘編程語言的申明&賦值方式,採用的是建立&指向的相似於指針的方式實現的。即Python中的變量其實是對值或者對象的一個指針(簡單的說他們是值得一個名字)。咱們來看一個例子。
{% highlight python %}
{% raw %}
p = 1
p = p+1
{% endraw %}
{% endhighlight %}
對於傳統語言,上面這段代碼的執行方式將會是,先在內存中申明一個p
的變量,而後將1
存入變量p
所在內存。執行加法操做的時候獲得2
的結果,將2
這個數值再次存入到p
所在內存地址中。可見整個執行過程當中,變化的是變量p
所在內存地址上的值
上面這段代碼中,Python其實是如今執行內存中建立了一個1
的對象,並將p
指向了它。在執行加法操做的時候,實際上經過加法操做獲得了一個2
的新對象,並將p
指向這個新的對象。可見整個執行過程當中,變化的是p
指向的內存地址函數
一句話來解釋:Python函數的參數默認值,是在編譯階段就綁定的。ui
如今,咱們先從一段摘錄來詳細分析這個陷阱的緣由。下面是一段從Python Common Gotchas中摘錄的緣由解釋:
Python’s default arguments are evaluated once when the function is defined, not each time the function is called (like it is in say, Ruby). This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well.
可見若是參數默認值是在函數編譯compile
階段就已經被肯定。以後全部的函數調用時,若是參數不顯示的給予賦值,那麼所謂的參數默認值不過是一個指向那個在compile
階段就已經存在的對象的指針。若是調用函數時,沒有顯示指定傳入參數值得話。那麼全部這種狀況下的該參數都會做爲編譯時建立的那個對象的一種別名存在。若是參數的默認值是一個不可變(Imuttable
)數值,那麼在函數體內若是修改了該參數,那麼參數就會從新指向另外一個新的不可變值。而若是參數默認值是和本文最開始的舉例同樣,是一個可變對象(Muttable
),那麼狀況就比較糟糕了。全部函數體內對於該參數的修改,實際上都是對compile
階段就已經肯定的那個對象的修改。
對於這麼一個陷阱在 Python官方文檔中也有特別提示:
Important warning: The default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes. For example, the following function accumulates the arguments passed to it on subsequent calls:
固然最好的方式是不要使用可變對象做爲函數默認值。若是非要這麼用的話,下面是一種解決方案。仍是以文章開頭的需求爲例:
{% highlight python %}
{% raw %}
def generate_new_list_with(my_list=None, element=None):
if my_list is None:
my_list = []
my_list.append(element)
return my_list
{% endraw %}
{% endhighlight %}
這個問題的答案在 StackOverflow 上能夠找到答案。這裏將得票數最多的答案最重要的部分摘錄以下:
Actually, this is not a design flaw, and it is not because of internals, or performance.
It comes simply from the fact that functions in Python are first-class objects, and not only a piece of code.
As soon as you get to think into this way, then it completely makes sense: a function is an object being evaluated on its definition; default parameters are kind of "member data" and therefore their state may change from one call to the other - exactly as in any other object.
In any case, Effbot has a very nice explanation of the reasons for this behavior in Default Parameter Values in Python.
I found it very clear, and I really suggest reading it for a better knowledge of how function objects work.
在這個回答中,答題者認爲出於Python編譯器的實現方式考慮,函數是一個內部一級對象。而參數默認值是這個對象的屬性。在其餘任何語言中,對象屬性都是在對象建立時作綁定的。所以,函數參數默認值在編譯時綁定也就不足爲奇了。
然而,也有其餘不少一些回答者不買帳,認爲即便是first-class object
也可使用closure
的方式在執行時綁定。
This is not a design flaw. It is a design decision; perhaps a bad one, but not an accident. The state thing is just like any other closure: a closure is not a function, and a function with mutable default argument is not a function.
甚至還有反駁者拋開實現邏輯,單純從設計角度認爲:只要是違背程序猿基本思考邏輯的行爲,都是設計缺陷!下面是他們的一些論調:
Sorry, but anything considered "The biggest WTF in Python" is most definitely a design flaw. This is a source of bugs for everyone at some point, because no one expects that behavior at first - which means it should not have been designed that way to begin with.
The phrases "this is not generally what was intended" and "a way around this is" smell like they're documenting a design flaw.
好吧,這麼看來,若是沒有來自於Python做者的親自陳清,這個問題的答案就一直會是一個謎了。