Python為什麼不是傳值或傳引用? - mathspp

banq發表於2021-11-12

本文解釋了為什麼 Python 不使用傳值系統,也不使用傳引用。
當你在 Python 中呼叫一個函式並給它一些引數時......它們是按值傳遞的嗎?不!引用?不!他們是透過分配賦值assignment傳遞的。
許多傳統的程式語言在向函式傳遞引數時採用以下兩種模型之一:
  • 一些語言使用傳值模型;和
  • 大多數其他人使用傳遞引用模型。

話雖如此,瞭解 Python 使用的模型很重要,因為這會影響您的程式碼的行為方式。
在這個 Pydon't 中,您將:
  • 看到 Python 不使用傳遞值也不使用傳遞引用模型;
  • 瞭解 Python 使用傳遞賦值模型;
  • 瞭解內建函式id;
  • 更好地理解 Python 物件模型;
  • 意識到每個物件都有 3 個非常重要的屬性來定義它;
  • 瞭解可變物件和不可變物件之間的區別;
  • 瞭解淺複製和深複製的區別;和
  • 瞭解如何使用該模組copy進行兩種型別的物件複製。

 

Python 是按值傳遞的嗎?
在傳值模型中,當您使用一組引數呼叫函式時,資料會被複制到函式中。這意味著您可以隨心所欲地修改引數,並且您將無法在函式之外更改程式的狀態。這不是 Python 所做的,Python 不使用值傳遞模型。
檢視下面的程式碼片段,它可能看起來像 Python 使用按值傳遞:

def foo(x):
    x = 4

a = 3
foo(a)
print(a)
# 3


這看起來像傳值模型,因為我們給了它 3,然後將其更改為 4,但是更改沒有反映在外部(結果a仍然是 3)。
但是,實際上,Python 並沒有將資料複製到函式中。
為了證明這一點,我將向您展示一個不同的函式:

def clearly_not_pass_by_value(my_list):
    my_list[0] = 42

l = [1, 2, 3]
clearly_not_pass_by_value(l)
print(l)
# [42, 2, 3]


正如我們所見,l列表的值,在函式clearly_not_pass_by_value呼叫以後發生了變化。因此,Python 不使用值傳遞模型。
 

Python 是按引用傳遞的嗎?
在真正的引用傳遞模型中,被呼叫函式可以訪問被呼叫者的變數!有時,它看起來像是 Python 所做的,但 Python 不使用傳遞引用模型。
我會盡力解釋為什麼這不是 Python 所做的:

def not_pass_by_reference(my_list):
    my_list = [42, 73, 0]

l = [1, 2, 3]
not_pass_by_reference(l)
print(l)
# [1, 2, 3]


如果 Python 使用傳遞引用模型,該函式將會完全改變l 函式外部的值,但正如我們所見,事實並非如此。
 

Python物件模型
要真正理解 Python 在呼叫函式時的行為方式,最好先了解 Python 物件是什麼,以及如何表徵它們。
在 Python 中,一切都是物件,每個物件都有三個特點:

  • 它的身份(唯一標識物件的整數,就像社會安全號碼標識人一樣);
  • 型別(標識您可以對物件執行的操作);和
  • 物件的內容。

這是一個物件及其三個特徵:

>>> id(obj)
2698212637504       # the identity身份 of `obj`
>>> type(obj)
<class 'list'>      # the type型別 of `obj`
>>> obj
<p class="indent">[1, 2, 3]           # the contents內容 of `obj`


物件的可變性取決於它的型別。換句話說,可變性是型別的特徵,而不是特定物件的特徵!
但是,物件可變究竟意味著什麼呢?或者一個物件是不可變的?
回想一下,物件的特徵在於其身份、型別和內容。如果您可以更改其物件的內容而不更改其標識和型別,則型別是可變的。
列表是可變資料型別的一個很好的例子。為什麼?因為列表是容器:您可以將內容放入列表中,也可以從相同列表中刪除內容。
在下面,您可以看到列表的內容如何在obj我們進行方法呼叫時發生變化,但列表的標識保持不變:

>>> obj = []
>>> id(obj)
2287844221184
>>> obj.append(0); obj.extend([1, 2, 3]); obj
<p class="indent">[42, 0, 1, 2, 3]
>>> id(obj)
2287844221184
>>> obj.pop(0); obj.pop(0); obj.pop(); obj
42
0
3
<p class="indent">[1, 2]
>>> id(obj)
2287844221184


然而,當處理不可變物件時,情況就完全不同了。如果我們查一下英語詞典,我們會得到“immutable”的定義:immutable——隨時間不變或無法改變。
不可變物件的內容永遠不會改變。以字串為例:
obj = "Hello, world!"
字串是本次討論的一個很好的例子,因為有時它們看起來是可變的。但他們不是!
一個物件是不可變的一個很好的指標是它的所有方法都返回一些東西,例如如果.append方法在列表上使用,則不會獲得返回值。另一方面,無論您在字串上使用什麼方法,結果都會返回給您:

>>> [].append(0)    # No return.
>>> obj.upper()     # A string is returned.
'HELLO, WORLD!"

請注意如何obj沒有自動更新為"HELLO, WORLD!". 相反,新字串已建立並返回給您。
字串不可變這一事實的另一個重要提示是您不能為其分配索引:

>>> obj[0]
'H'
>>> obj[0] = "h"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment


這表明,當一個字串被建立時,它保持不變。它可用於構建其他字串,但字串本身總是如此不變。
int、float、bool、str、tuple和complex是最常見的不可變物件型別; list、set和dict是最常見的可變物件型別。
 

變數名作為標籤
另一個需要理解的重要事情是變數名與物件本身幾乎沒有關係。
實際上,名稱obj只是我決定附加到標識為 2698212637504、具有列表型別和內容 1、2、3 的物件的標籤。
就像我obj給那個物件貼上標籤一樣,我可以給它貼上更多的名字:
>>> foo = bar = baz = obj
同樣,這些名稱只是標籤。我決定貼在同一個物體上的標籤。我們怎麼知道它是同一個物件?好吧,他們所有的“社會安全號碼”(id)都匹配,所以他們必須是同一個物件:

>>> id(foo)
2698212637504
>>> id(bar)
2698212637504
>>> id(baz)
2698212637504
>>> id(obj)
2698212637504


因此,我們得出結論,foo、bar、baz、 和obj是所有引用同一個物件的變數名。
 

運算子is
它檢查兩個物件是否相同。
要使兩個物件相同,它們必須具有相同的標識:

>>> foo is obj
True
>>> bar is foo
True
>>> obj is foo
True


作為暱稱的分配
分配變數就像給某人一個新的暱稱。我的中學朋友叫我“羅傑”。我的大學朋友叫我“Girão”。我不熟悉的人用我的名字稱呼我——“羅德里戈”。然而,不管他們叫我什麼,我還是我,對吧?
如果有一天我決定改變我的髮型,每個人都會看到新發型,不管他們叫我什麼!
以類似的方式,如果我修改物件的內容,我可以使用我喜歡的任何暱稱來檢視這些更改發生的情況。例如,我們可以更改我們一直在玩的列表的中間元素:

>>> foo[1] = 42
>>> bar
<p class="indent">[1, 42, 3]
>>> baz
<p class="indent">[1, 42, 3]
>>> obj
<p class="indent">[1, 42, 3]


我們使用暱稱foo來修改中間元素,但該更改也可以從所有其他暱稱中看到。
為什麼?
因為它們都指向同一個列表物件。
 

Python 是按分配賦值傳遞的
完成所有這些之後,我們現在準備瞭解 Python 如何將引數傳遞給函式。
當我們呼叫一個函式時,函式的每個引數都被分配給傳入的物件。本質上,現在每個引數都成為了傳入物件的新暱稱。

  • 不可變引數

如果我們傳入不可變的引數,那麼我們就無法修改引數本身。畢竟,這就是不可變的意思:“不會改變”。
這就是為什麼它看起來像 Python 使用按值傳遞模型的原因。因為我們可以讓引數儲存其他東西的唯一方法是將它分配給一個完全不同的東西。當我們這樣做時,我們對不同的物件重複使用相同的暱稱:

def foo(bar):
    bar = 3
    return bar

foo(5)


在上面的例子中,當我們使用引數5呼叫foo時,就好像我們在函式的開頭bar = 5做的一樣。
緊接著,我們有了bar = 3. 這意味著“取暱稱“bar”並將其指向整數3“。Python 並不關心bar,因為暱稱(作為變數名)已經被使用了。它現在指向那個3!
  • 可變引數

另一方面,可以更改可變引數。我們可以修改它們的內部內容。可變物件的一個​​主要例子是列表:它的元素可以改變(長度也可以)。
這就是為什麼它看起來像 Python 使用傳遞引用模型的原因。然而,當我們改變一個物件的內容時,我們並沒有改變這個物件本身的身份。同樣,當你改變你的髮型和你的衣服,你的社會安全號碼並沒有改變:

>>> l = [42, 73, 0]
>>> id(l)
3098903876352
>>> l[0] = -1
>>> l.append(37)
>>> id(l)
3098903876352


 

相關文章