你真的知道Python中的賦值與拷貝嗎?

史善孟發表於2021-02-04

Python中的賦值


  • Python是一種動態型別程式語言,與C/C++等靜態型別程式語言不同,Python中的變數沒有固定的型別,變數也無需提前宣告分配記憶體空間,因此我們可以給任何變數賦值任何符合Python規範的型別值,舉個例子:
    互動式Python程式碼:

    >>> var = 88
    >>> var = 'hello world'
    

    在C語言中,變數使用前需要提前宣告並明確其型別(或宣告和賦值可同一條語句執行),以便為其分配記憶體空間,舉個例子:
    C程式碼:

    int a;
    a = 100;
    

    但在C語言中一般不可直接將其他型別值賦值給不同型別的變數(可強制型別轉換的除外),例如如下程式碼就會報錯:
    C程式碼:

    int a;
    a = "hello world";
    
  • 在Python中將字面量或變數對另一個變數進行賦值可以理解為:給記憶體中的變數實體貼上一個標籤。這個標籤就是變數識別符號,舉個例子:
    互動式Python程式碼:

    >>> a = 100
    >>> b = a
    >>> a is b
    True
    

    上述程式碼表明變數a與變數b是同一個物件,識別符號a和識別符號b都是記憶體中變數實體100的一個標籤。在C語言中,因為每個變數都有自己的記憶體空間,變數賦值是將值複製到指定的記憶體中,因此進行賦值的兩個變數只是值相同,但並不表示同一個物件。舉個例子:
    C程式碼:

    #include <stdio.h>
    int a = 100;
    int b;
    b = a;  
    if (&a == &b)
    {
        printf("yse\n");
    }
    else
    {
        printf("no\n");
    }
    // 輸出:no
    
  • Python中函式的實參和形參之間以及返回值也是進行賦值操作的,舉個例子:
    互動式Python程式碼:

    >>> a = 3
    >>> b = 5
    >>> def func(m, n):
            print(m is a)
            print(n is b)
            r = m + n
            print('id(r):', id(r))
            return r
    >>> c = func(a, b)
    True
    True
    id(r): 10914624
    >>> id(c)
    10914624
    

    根據上述程式碼可知,Python函式呼叫時形參會被實參進行賦值操作,即在執行c = func(a, b)時其實相當於在函式內部執行了m = an = b。那麼就會出現一個問題,當實參為可變物件時,在函式內部就存在改變實參的風險,舉個例子:
    互動式Python程式碼:

    >>> a = [1, 2]
    >>> def func(List):
            List.append(a[0] + a[1])
            return List
    >>> c = func(a)
    >>> print(c)
    [1, 2, 3]
    >>> print(a)
    [1, 2, 3]
    >>> id(a) == id(c)
    True
    

    上述程式碼中a為列表型別(可變物件),在函式內部List新增了一個元素並返回。aList和函式返回後的c表示同一個物件,這是我們之前討論過的。現在我們討論的重點是在函式內部改變了函式的實參,當然在有些情況下,我們正是需要藉助這種特性完成某種功能,這種情況我們暫且不討論。但在實際的工作中,更多的情況是我們不想函式更改實參,針對這種情況我們應該怎樣規避實參被更改的風險呢?有三種方法:

    • 儘量使用不可變型別作實參
      當實參為不可變物件時,函式內部形參的操作不會影響實參,舉個例子:
      互動式Python程式碼:

      >>> a = 55
      >>> id(a)
      10916224
      >>> def func(value):
              print('id(value):', id(value), 'value: ', value)
              print(value is a)
              value = 99
              print('id(value):', id(value), 'value:', value)
              print(value is a)
      >>> func(a)
      id(value): 10916224 value:  55
      True
      id(value): 10917632 value:  99
      False
      >>> print('id(a):', id(a), 'a:', a)
      id(a): 10916224 a: 55
      

      上述程式碼中a為int型別(不可變型別),作為實參賦值給形參value,函式在執行value = 99之前valuea表示同一個物件,在執行之後valuea表示不同的物件,其實這很好理解就像我們前面討論的,在執行賦值語句之後value識別符號被“貼”到另一個記憶體變數實體99上了。

    • 使用拷貝

    • 以上兩種都不想用?那就寫程式碼的時候多注意吧

    下面我就來討論今天的第二個重點:拷貝

Python中的拷貝


Python中的拷貝分為淺拷貝深拷貝

  • 淺拷貝

    Python中的預設拷貝都是淺拷貝,這裡我們就拿列表型別舉個例子,先看看列表有哪些拷貝方法,此處參考Python語言及其應用
    互動式Python程式碼:

    >>> a = [1, 2, 3]
    >>> b = a.copy()
    >>> a
    [1, 2, 3]
    >>> b
    [1, 2, 3]
    >>> b is a
    False
    >>> c = list(a)
    >>> c
    [1, 2, 3]
    >>> c is a
    False
    >>> d = a[:]
    >>> d is a
    False
    

    上述程式碼中顯示出三種列表拷貝方法:

    • 使用列表物件的.copy()方法
    • 使用list()轉換函式
    • 使用列表分片[:]

    我們也可看到拷貝出來的物件與被拷貝物件不再是同一個物件,這也是拷貝的根本目的。當使用可變物件的拷貝進行函式傳參可以避免函式內部修改實參,舉個例子:
    互動式Python程式碼:

    >>> a = [1, 2, 3]
    >>> def func(List):
            List.append(100)
            List[0] = 'hello'
            return List
    >>> b = func(a.copy())
    >>> b
    ['hello', 2, 3, 100]
    >>> a
    [1, 2, 3]
    

    好像一切都很完美,達到了我們的目的。於是我們編寫了下面這樣的程式碼:
    互動式Python程式碼:

    >>> a = [1, 'hello', 'world', [88, 99], 22]
    >>> def func(List):
            List.append('shawn')
            List[0] = 'simon'
            List[3][0] = 100
            return List
    >>> b = func(a.copy())
    >>> b
    ['simon', 'hello', 'world', [100, 99], 22, 'shawn']
    >>> a
    [1, 'hello', 'world', [100, 99], 22]
    

    有瑕疵啊,這執行結果和我們想象的不一樣,我們想象著變數a索引為3的元素值仍為[88, 99],但輸出並不是這樣,問題出在了哪裡?原因在於我們前面所說的Python中的預設拷貝均為淺拷貝,如果我們想要輸出達到我們的預期,則需要深拷貝

  • 深拷貝
    深拷貝就是在每個層次都對可變物件進行拷貝,淺拷貝只對最外層可變物件進行拷貝。
    我們先來看看賦值的情況即b = a,如下圖所示:
    enter image description here
    示意圖ab指向同一個物件,我們通過程式碼驗證其正確性:
    互動式Python程式碼:

    >>> a = [1, 2, [3, 4]]
    >>> b = a
    >>> id(b) == id(a)
    True
    >>> id(a[0]) == id(b[0])
    True
    >>> id(a[1]) == id(b[1])
    True
    >>> id(a[2]) == id(b[2])
    True
    >>> id(a[2][0]) == id(b[2][0])
    True
    >>> id(a[2][1]) == id(b[2][1])
    True
    

    再來看看淺拷貝的情況,先上圖:
    enter image description here
    上圖中為b = a[:]示意圖,ba的淺拷貝,淺拷貝會將最外層可變物件進行拷貝,下面進行程式碼驗證:
    互動式Python程式碼:

    >>> a = [1, 2, [3, 4]]
    >>> b = a[:]
    >>> id(b) == id(a)
    False
    >>> id(a[0]) == id(b[0])
    True
    >>> id(a[1]) == id(b[1])
    True
    >>> id(a[2]) == id(b[2])
    True
    >>> id(a[2][0]) == id(b[2][0])
    True
    >>> id(a[2][1]) == id(b[2][1])
    True
    

    最後來看看深拷貝,深拷貝我們需要用到copy庫的deepcopy,示意圖如下:
    enter image description here 接下來進行程式碼驗證:
    互動式Python程式碼:

    >>> from copy import deepcopy
    >>>
    >>> a = [1, 2, [3, 4]]
    >>> b = deepcopy(a)
    >>> id(a) == id(b)
    False
    >>> id(a[0]) == id(b[0])
    True
    >>> id(a[1]) == id(b[1])
    True
    >>> id(a[2]) == id(b[2])
    False
    >>> id(a[2][0]) == id(b[2][0])
    True
    >>> id(a[2][1]) == id(b[2][1])
    True
    

    因此當實參為可變物件且其中含有多層巢狀的可變物件時,如果我們不想因為我們的粗心讓函式更改實參,那麼我們就應該使用深拷貝,看程式碼:
    互動式Python程式碼:

    >>> a = [1, 2, [3, 4]]
    >>> def func(List):
            List.append('hello world')
            List[0] = 'shawn'
            List[2][0] = 'simon'
            List[2][1] = 199
            return List
    >>> from copy import deepcopy
    >>> b = func(deepcopy(a))
    >>> a
    [1, 2, [3, 4]]
    >>> b
    ['shawn', 2, ['simon', 199], 'hello world']
    

    Note: Python中的深拷貝和淺拷貝只是針對可變物件而言,而對於不可變物件無所謂深拷貝還是淺拷貝,因為對於不可變物件兩種拷貝和變數之間賦值沒什麼兩樣。
    互動式Python程式碼:

    >>> from copy import deepcopy
    >>>
    >>> a = 'hello world'
    >>> b = deepcopy(a)
    >>> c = a
    >>> id(a) == id(b) == id(c)
    True
    

總結


  • Python中變數之間的賦值,相當於給變數多貼了一個“標籤”,或者說給變數起了個別名

  • 深拷貝、淺拷貝是針對可變物件說的,不可變物件無所謂什麼拷貝

  • 對於可變物件,深拷貝對每層可變物件都進行拷貝,而淺拷貝只對最外層進行拷貝

  • 當函式實參為可變物件且我們不想函式改變實參本身,應儘量使用深拷貝,尤其在實參中巢狀了可變物件時

相關文章