還記得上一次關於變量作用域文章最後的問題嗎?
deffunc(m):
m[0] = 20
m = [4, 5, 6]
return m
l = [1, 2, 3]
func(l)
print('l =', l)
實際的輸出我想大家都嘗試過了吧,應該是:
[20, 2, 3]
和80%人想象中的結果不一樣。
這是為什麽呢?
在 Python 的官方文件 FAQ 裏有這樣一句話
Remember that arguments are passed by assignment in Python.
要記住,Python 裏的參數是透過賦值傳遞的。
https://docs.python.org/3/faq/programming.html#how-do-i-write-a-function-with-output-parameters-call-by-reference
所以要弄清楚參數傳遞,先得弄清 Python 的賦值。
或許在很多人的直觀印象中,變量是一個容器;給變量賦值,就像是往一個儲存的容器中填入一個數據;再次賦值就是把容器中的數據換掉。
然而,
在 Python 中,這種理解是錯的!
在 Python 中,這種理解是
錯
的!
在 Python 中,這種理解是
錯
的!
若是想要個形象的類比, Python 中的變量更像是是個標簽;給變量賦值,就是把標簽貼在一個物體上;再次賦值就是把標簽貼在另一個物體上 。
體會下這兩種設計的差異:
· 前者,變量是一個固定的存在,賦值只會改變其中的數值,而變量本身沒有改動。
· 後者,變量不存在實體,它僅僅是一個標簽,一旦賦值就被設定到另一個物體上,不變的是那些物體。
這些「物體」就是 物件 。 Python 中所有東西都是物件 ,包括函式、類、模組,甚至是字串’hello’,數位1、2、3,都是物件。
用個例子來說明:
a = 1
b = 2
c = 1
# 再次賦值
a = b
在這個程式碼裏,a 和 c 其實指向的是同一個物件—整數 1。給 a 賦值為 b 之後,a 就變成了指向 2 的標簽,但 1 和 c 都不會受影響。
示意圖:
更有說服力一點的驗證:
a = 1
print('a', a, id(a))
b = 2
print('b', b, id(b))
c = 1
print('c', c, id(c))
# 再次賦值
a = b
print('a', a, id(a))
輸出:
a 1 4301490544
b 2 4301490576
c 1 4301490544
a 2 4301490576
id() 可以認為是獲取一個物件的地址。可以看出,a 和 c 開始其實是同一個地址,而後來賦值之後,a 又和 b 是同一個地址。
每次給變量重新賦值,它就指向了新的地址,與原來的地址無關了。
回到函式的呼叫上:
Python 裏的參數是透過賦值傳遞的
deffn(x):
x = 3
a = 1
fn(a)
print(a)
輸出結果為
1
,a 沒有變化。
呼叫 fn(a) 的時候,就相當於做了一次
x = a
,把 a 賦值給了 x,也就是把 x 這個標簽貼在了 a 的物件上。只不過 x 的作用域僅限於函式 fn 內部。
當 x 在函式內部又被賦值為 3 時,就是把 x 又貼在了 3 這個物件上,與之前的 a 不在有關系。所以外部的 a 不會有任何變化。
把其中的數值換成其他物件,效果也是一樣的:
deffn(x):
x = [4,5,6]
a = [1,2,3]
fn(a)
print(a)
輸出結果為
[1,2,3]
,a 沒有變化。(記住這個例子,最後我們還會提到)
那上次的題目又是怎麽回事?
我們再來看一個賦值:
a = [1,2,3]
print('a', a, id(a))
b = a
print('b', b, id(b))
b[1] = 5
print('a', a, id(a))
print('b', b, id(b))
輸出:
a [1, 2, 3] 4490723464
b [1, 2, 3] 4490723464
a [1, 5, 3] 4490723464
b [1, 5, 3] 4490723464
這個是不是好理解一點?b 賦值為 a 後,和 a 指向同一個列表物件。[1] 這個基於 index 的賦值是 list 物件本身的一種操作,並沒有給 b 重新貼標簽,改變的是物件本身。所以 b 指向的還是原來的物件,此物件的改動自然也會體現在 a 身上。同理,
b.append(7)
這樣的操作也會是類似的效果。
再來回顧下原問題呢:
deffunc(m):
m[0] = 20
# m = [4, 5, 6]
return m
l = [1, 2, 3]
func(l)
print('l =', l)
去掉那句
m=[4,5,6]
的幹擾,函式的呼叫就相當於:
l = [1, 2, 3]
m = l
m[0] = 20
l 的值變成
[20,2,3]
沒毛病吧。而對 m 重新賦值之後,m 與 l 無關,但不影響已經做出的修改。
這就是這道題的解答。上次留言裏有些同學已經解釋的很準確了。
另外說下, 函式的返回值 return,也相當於是一次賦值 。只不過,這時候是把函式內部返回值所指向的物件,賦值給外面函式的呼叫者:
deffn(x):
x = 3
print('x', x, id(x))
return x
a = 1
a = fn(a)
print('a', a, id(a))
輸出:
x 3 4556777904
a 3 4556777904
函式結束後,x 這個標簽雖然不存在了,但 x 所指向的物件依然存在,就是 a 指向的新物件。
所以,如果你想要透過一個函式來修改外部變量的值,有幾種方法:
透過返回值賦值
使用全域變量
修改 list 或 dict 物件的內部元素
修改類的成員變量
有相當多的教程把 Python 的函式參數傳遞分為可變物件和不可變物件(這個概念下次來說)來說明,然後類比到 C++ 的值傳遞和參照傳遞。我很反對這樣去理解:
對於沒有學過 C++ 的人來說,這個解釋屬於迴圈論證,還是沒說清問題。
Python 本來就不存在值傳遞/參照傳遞的概念,這個比較沒有意義。
這個類比實際上是錯誤的。就算類比,也應該是相當於 C++ 裏的指標值傳遞。
用可變物件/不可變物件來劃分很容易產生誤解,比如我們前面例子中的
x=[4,5,6]
,它是可變物件,但一樣不影響外部參數的值。
這點前面貼出的官方文件裏也直說了:
Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per se.
賦值是建立了一份物件的參照(也就是地址),形參和實參之間不存在別名的關系,本質上不存在參照傳遞。
網上很容易搜到「參數是可變物件就相當於參照傳遞」這種錯誤的理解。也不知道他們是對 Python 的參數傳遞有什麽誤解,還是對C++的參照傳遞有什麽誤解。結果就是,讓很多初學者從網上看了幾篇教程之後,更糊塗了。
所以呢,找到一個靠譜的教程是非常重要滴😏
作者:Crossin的編程教室
Crossin的新書【 碼上行動:用ChatGPT學會Python編程 】已經上市了。 本書以ChatGPT為輔助,系統全面地講解了如何掌握Python編程,適合Python零基礎入門的讀者學習。
購買後可加入讀者交流群,Crossin為你開啟陪讀模式,解答你在閱讀本書時的一切疑問。
添加微信 crossin123 ,加入編程教室共同學習 ~
感謝 轉發 和 點贊 的各位~