摘要:另外說下,函數的返回值,也相當于是一次賦值。只不過,這時候是把函數內部返回值所指向的對象,賦值給外面函數的調用者輸出函數結束后,這個標簽雖然不存在了,但所指向的對象依然存在,就是指向的新對象。
還記得上一次關于變量作用域文章 :
Crossin:全菊變量和菊部變量zhuanlan.zhihu.com
我們在公眾號(Crossin的編程教室)里做了個問題投票:
def func(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 里的參數是通過賦值傳遞的
def fn(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 不會有任何變化。
把其中的數值換成其他對象,效果也是一樣的:
def fn(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) 這樣的操作也會是類似的效果。
再來回顧下原問題呢:
def func(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,也相當于是一次賦值 。只不過,這時候是把函數內部返回值所指向的對象,賦值給外面函數的調用者:
def fn(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++的引用傳遞有什么誤解。結果就是,讓很多初學者從網上看了幾篇教程之后,更糊涂了。
所以呢,找到一個靠譜的教程是非常重要滴
════
其他文章及回答:
如何自學Python | 新手引導 | 精選Python問答 | Python單詞表 | 區塊鏈 | 人工智能 | 雙11 | 嘻哈 | 爬蟲 | 排序算法 | 我用Python | 高考 | 世界杯
歡迎搜索及關注: Crossin的編程教室
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/42456.html
摘要:不可變對象不允許對自身內容進行修改。因為他們說到不可變對象時用的是賦值,而說到可變對象又用了的索引等方法,這根本是兩碼事。基于這一設定,兩者在功能上的最大區別就是不可變對象可以作為字典的鍵,而可變對象不行。 前陣子我們聊了下函數的參數傳遞以及變量賦值的一些內容:關于函數參數傳遞,80%人都錯了 簡單回顧下要點: 1. Python 中的變量不是裝有對象的 容器 ,而是貼在對象上的 標簽...
摘要:之前關于的作用域賦值參數傳遞,我們接連談了幾篇文章全菊變量和菊部變量關于函數參數傳遞,人都錯了可變對象與不可變對象今天我們依然要就相關話題繼續下去。這是由于它們是不可變對象,不存在被修改的可能,所以拷貝和賦值是一樣的。 之前關于 Python 的作用域、賦值、參數傳遞,我們接連談了幾篇文章: 全菊變量和菊部變量 關于函數參數傳遞,80%人都錯了 可變對象與不可變對象 今天我們依然要...
閱讀 2316·2021-11-23 10:09
閱讀 2891·2021-10-12 10:11
閱讀 2599·2021-09-29 09:35
閱讀 1342·2019-08-30 15:53
閱讀 2266·2019-08-30 11:15
閱讀 2910·2019-08-29 13:01
閱讀 2295·2019-08-28 18:15
閱讀 3367·2019-08-26 12:13