1. 程式人生 > >python 傳值 傳引用 可變物件 不可變物件 的區別與聯絡

python 傳值 傳引用 可變物件 不可變物件 的區別與聯絡

可變物件 與 不可變物件

我們知道在python中一切皆物件。在python世界中可以把物件大體分成兩大類:

  • 不可變物件:數字(int,float, double)、字串、元組(tuple)、function等
  • 可變物件:字典(dict)、列表(list)、集合(set)、程式自定義的物件

所謂不可變物件就是物件的內容不能改變。給人感覺有點像c++的const修飾的物件。但是我們可以對一個不可變物件進行賦值(寫操作),這時候這個不可變的物件就指向了一個新的地址空間,指向的內容也更新成新賦值的內容

可變物件就是對物件本身的修改(寫操作)是直接作用於物件自身的,不會產生新的物件

通過內建函式id()來獲取變數的地址,下面舉例說明:

num = 1
print "num addr:",id(num)
num += 2
print "num addr:",id(num)

num addr: 74024648
num addr: 74024624

執行num += 2給不可變物件num賦值的時候會產生一個新的地址空間,並用新的內容填充這塊地址空間,正如上面的輸出所示,此時的num的地址已經改變

下面再看一個

listnum = [1,23,2]
print "list addr:",id(listnum)
listnum.append(11)
print "list addr:",id(listnum)
print listnum

list addr: 79300608
list addr: 79300608
[1, 23, 2, 11]
list是可變物件,修改一個可換物件可直接作用於這個可變物件,所以最終的list就插入了一個元素

傳值還是傳引用

有了前面的可變物件與不可變物件的認識,我們在就很容易理解傳值還是傳引用的問題。但是先說個結論:python中不存在所謂的傳值的函式呼叫,一切都是傳遞引用

我們知道函式傳遞的引數是進行從實參到形參的賦值操作進行的。在python中我們知道,不管是可變物件還是不可變物件,所有的賦值操作並沒有產生實質性的東西,新賦值的物件和源物件的地址始終是一樣的,也就是說兩者在賦值操作完成後指向的是同一塊記憶體。我們可以看下面的例子:
num = 1
num_bk = num
print "num addr:%d   num_bk addr:%d" %(id(num),id(num_bk))

listnum = [1,23,2]
listnum_bk = listnum
print "listnum addr:%d   listnum_bk addr:%d"%(id(listnum),id(listnum_bk))


num addr:77104840   num_bk addr:77104840
listnum addr:79104000   listnum_bk addr:79104000

可以看出不管是可變物件還是不可變物件,賦值後兩個物件的地址是相同的。也證實了上面的結論。所以問題的關鍵就變成了我們對新賦值的物件進行修改是否會影響到原物件的值。這個問題似乎回到了上面的對可變物件和不可變物件的寫操作的問題。

通過上面的講解我們知道:

  • 對可變物件進行寫操作是直接作用於寫物件本身。
  • 對不可變物件進行寫操作會產生一個新的地址空間,並用新的內容填充這塊地址空間

到此我們應該很容易知道:

  • 當我們傳遞一個不可變物件的引數到函式中(相當於通過賦值給函式的形參),如果在函式中對這個物件進行修改(寫操作)是不會改變原始物件的值,因為會產生一個新的物件,這樣的呼叫給人感覺是傳值的,但其實原理不一樣。
  • 當我們傳遞一個可變物件到函式中,如果函式中對這個物件進行修改(寫操作),這個是會影響到原來的物件的 看下面具體的例子:
def UpdateNum(num):
    print "begin update num addr:",id(num)
    num += 2
    print "after uodate num addr:",id(num)

num = 1
print "begin call function num add:",id(num)
UpdateNum(num)
print "after call function num add:",id(num)


begin call function num add: 77104840
begin update num addr: 77104840
after uodate num addr: 77104816
after call function num add: 77104840

如何實現可變物件的傳值和不可變物件的傳引用

這個問題這樣表述可能有點問題,但是意思可能都明白。就是想實現,傳入的是可變物件時,函式內部的修改不會影響到原來的物件;傳入是不可變物件,函式內部的修改能夠影響到原來的物件

對於後者的解決方案比較簡單,就是函式通過返回值回傳這個新的物件,呼叫方用原來的物件來接收這個新的物件就達到了這個效果。但是要實現可變物件的傳值問題就要藉助於複製的內建函數了。這個介紹兩個拷貝函式:copy(淺拷貝)、deepcopy(深拷貝)


淺拷貝

淺拷貝不會拷貝子物件,只是拷貝了子物件的引用,下面舉例說明:
listnum = [1,2,[11,22],3]
listcopy = copy.copy(listnum)

listcopy.append(4)
print "list value :", listnum
print "listcopy value:", listcopy

listnum[2].append(33)
print "list value :", listnum
print "listcopy value:", listcopy


list value : [1, 2, [11, 22], 3]
listcopy value: [1, 2, [11, 22], 3, 4]
list value : [1, 2, [11, 22, 33], 3]
listcopy value: [1, 2, [11, 22, 33], 3, 4]
可以看到我們子物件listnum[2]的修改會作用到複製的兩個物件,而深拷貝就是解決這個問題的

深拷貝

相對於淺拷貝而言,深拷貝會連子物件的記憶體也會拷貝一份,對子物件的修改不會影響到源物件,還是剛才的例子
listnum = [1,2,[11,22],3]
listcopy = copy.deepcopy(listnum)

listcopy.append(4)
print "list value :", listnum
print "listcopy value:", listcopy

listnum[2].append(33)
print "list value :", listnum
print "listcopy value:", listcopy


list value : [1, 2, [11, 22], 3]
listcopy value: [1, 2, [11, 22], 3, 4]
list value : [1, 2, [11, 22, 33], 3]
listcopy value: [1, 2, [11, 22], 3, 4]

可以看到所有的修改都是獨立的

熟悉c++的同學可能對深淺拷貝理解起來更容易。深淺拷貝的區別就是對指標成員物件進行的是僅僅指標的複製還是對指標所指示的記憶體空間進行復制。僅複製指標的話,由於兩個指標同時指向同一塊記憶體,所以修改是同步的