轉(zhuǎn)自:http://cenalulu.github.io/python/default-mutable-arguments/
本文將介紹使用mutable對(duì)象作為Python函數(shù)參數(shù)默認(rèn)值潛在的危害,以及其實(shí)現(xiàn)原理和設(shè)計(jì)目的
我們就用實(shí)際的舉例來(lái)演示我們今天所要討論的主要內(nèi)容。
下面一段代碼定義了一個(gè)名為generate_new_list_with
的函數(shù)。該函數(shù)的本意是在每次調(diào)用時(shí)都新建一個(gè)包含有給定element
值的list。而實(shí)際運(yùn)行結(jié)果如下:
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]
>>>
可見(jiàn)代碼運(yùn)行結(jié)果并不和我們預(yù)期的一樣。list_2
在函數(shù)的第二次調(diào)用時(shí)并沒(méi)有得到一個(gè)新的list并填入2,而是在第一次調(diào)用結(jié)果的基礎(chǔ)上append了一個(gè)2。為什么會(huì)發(fā)生這樣在其他編程語(yǔ)言中簡(jiǎn)直就是設(shè)計(jì)bug一樣的問(wèn)題呢?
要了解這個(gè)問(wèn)題的原因我們先需要一個(gè)準(zhǔn)備知識(shí),那就是:Python變量到底是如何實(shí)現(xiàn)的?
Python變量區(qū)別于其他編程語(yǔ)言的申明&賦值方式,采用的是創(chuàng)建&指向的類(lèi)似于指針的方式實(shí)現(xiàn)的。即Python中的變量實(shí)際上是對(duì)值或者對(duì)象的一個(gè)指針(簡(jiǎn)單的說(shuō)他們是值得一個(gè)名字)。我們來(lái)看一個(gè)例子。
p = 1
p = p+1
對(duì)于傳統(tǒng)語(yǔ)言,上面這段代碼的執(zhí)行方式將會(huì)是,先在內(nèi)存中申明一個(gè)p
的變量,然后將1
存入變量p
所在內(nèi)存。執(zhí)行加法操作的時(shí)候得到2
的結(jié)果,將2
這個(gè)數(shù)值再次存入到p
所在內(nèi)存地址中。可見(jiàn)整個(gè)執(zhí)行過(guò)程中,變化的是變量p
所在內(nèi)存地址上的值
上面這段代碼中,Python實(shí)際上是現(xiàn)在執(zhí)行內(nèi)存中創(chuàng)建了一個(gè)1
的對(duì)象,并將p
指向了它。在執(zhí)行加法操作的時(shí)候,實(shí)際上通過(guò)加法操作得到了一個(gè)2
的新對(duì)象,并將p
指向這個(gè)新的對(duì)象。可見(jiàn)整個(gè)執(zhí)行過(guò)程中,變化的是p
指向的內(nèi)存地址
一句話(huà)來(lái)解釋?zhuān)篜ython函數(shù)的參數(shù)默認(rèn)值,是在編譯階段就綁定的。
現(xiàn)在,我們先從一段摘錄來(lái)詳細(xì)分析這個(gè)陷阱的原因。下面是一段從Python Common Gotchas中摘錄的原因解釋?zhuān)?/p>
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.
可見(jiàn)如果參數(shù)默認(rèn)值是在函數(shù)編譯compile
階段就已經(jīng)被確定。之后所有的函數(shù)調(diào)用時(shí),如果參數(shù)不顯示的給予賦值,那么所謂的參數(shù)默認(rèn)值不過(guò)是一個(gè)指向那個(gè)在compile
階段就已經(jīng)存在的對(duì)象的指針。如果調(diào)用函數(shù)時(shí),沒(méi)有顯示指定傳入?yún)?shù)值得話(huà)。那么所有這種情況下的該參數(shù)都會(huì)作為編譯時(shí)創(chuàng)建的那個(gè)對(duì)象的一種別名存在。如果參數(shù)的默認(rèn)值是一個(gè)不可變(Imuttable
)數(shù)值,那么在函數(shù)體內(nèi)如果修改了該參數(shù),那么參數(shù)就會(huì)重新指向另一個(gè)新的不可變值。而如果參數(shù)默認(rèn)值是和本文最開(kāi)始的舉例一樣,是一個(gè)可變對(duì)象(Muttable
),那么情況就比較糟糕了。所有函數(shù)體內(nèi)對(duì)于該參數(shù)的修改,實(shí)際上都是對(duì)compile
階段就已經(jīng)確定的那個(gè)對(duì)象的修改。
對(duì)于這么一個(gè)陷阱在 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:
當(dāng)然最好的方式是不要使用可變對(duì)象作為函數(shù)默認(rèn)值。如果非要這么用的話(huà),下面是一種解決方案。還是以文章開(kāi)頭的需求為例:
def generate_new_list_with(my_list=None, element=None):
if my_list is None:
my_list = []
my_list.append(element)
return my_list
這個(gè)問(wèn)題的答案在 StackOverflow 上可以找到答案。這里將得票數(shù)最多的答案最重要的部分摘錄如下:
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.
在這個(gè)回答中,答題者認(rèn)為出于Python編譯器的實(shí)現(xiàn)方式考慮,函數(shù)是一個(gè)內(nèi)部一級(jí)對(duì)象。而參數(shù)默認(rèn)值是這個(gè)對(duì)象的屬性。在其他任何語(yǔ)言中,對(duì)象屬性都是在對(duì)象創(chuàng)建時(shí)做綁定的。因此,函數(shù)參數(shù)默認(rèn)值在編譯時(shí)綁定也就不足為奇了。
然而,也有其他很多一些回答者不買(mǎi)賬,認(rèn)為即使是first-class object
也可以使用closure
的方式在執(zhí)行時(shí)綁定。
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.
甚至還有反駁者拋開(kāi)實(shí)現(xiàn)邏輯,單純從設(shè)計(jì)角度認(rèn)為:只要是違背程序猿基本思考邏輯的行為,都是設(shè)計(jì)缺陷!下面是他們的一些論調(diào):
> 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.
好吧,這么看來(lái),如果沒(méi)有來(lái)自于Python作者的親自陳清,這個(gè)問(wèn)題的答案就一直會(huì)是一個(gè)謎了。
聯(lián)系客服