當前位置: 妍妍網 > 辦公

Python到底是參照傳遞還是值傳遞?網上大多數教程都講錯了

2024-06-24辦公

還記得上一次關於變量作用域文章最後的問題嗎?

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 指向的新物件。

所以,如果你想要透過一個函式來修改外部變量的值,有幾種方法:

  1. 透過返回值賦值

  2. 使用全域變量

  3. 修改 list 或 dict 物件的內部元素

  4. 修改類的成員變量

有相當多的教程把 Python 的函式參數傳遞分為可變物件和不可變物件(這個概念下次來說)來說明,然後類比到 C++ 的值傳遞和參照傳遞。我很反對這樣去理解:

  1. 對於沒有學過 C++ 的人來說,這個解釋屬於迴圈論證,還是沒說清問題。

  2. Python 本來就不存在值傳遞/參照傳遞的概念,這個比較沒有意義。

  3. 這個類比實際上是錯誤的。就算類比,也應該是相當於 C++ 裏的指標值傳遞。

  4. 用可變物件/不可變物件來劃分很容易產生誤解,比如我們前面例子中的 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 ,加入編程教室共同學習 ~

感謝 轉發 點贊 的各位~