Python 對象拷貝的詳細教程

来源:https://www.cnblogs.com/djdjdj123/archive/2023/08/12/17625029.html
-Advertisement-
Play Games

在本篇文章中,會先介紹 Python 中對象的基礎概念,之後會提到對象的深淺拷貝以及區別。在閱讀後,應該掌握如下的內容: - 理解變數、引用和對象的關係 - 理解 Python 對象中 identity,type 和 value 的概念 - 什麼是 mutable 和 immutable 對象?以及 ...


在本篇文章中,會先介紹 Python 中對象的基礎概念,之後會提到對象的深淺拷貝以及區別。在閱讀後,應該掌握如下的內容:

  • 理解變數、引用和對象的關係

  • 理解 Python 對象中 identity,type 和 value 的概念

  • 什麼是 mutable 和 immutable 對象?以及它們和 hashable 的關係

  • 深淺拷貝的過程以及區別

1.變數,引用和對象

變數無類型,它的作用僅僅在某個時候引用了特定的對象而已,具體在記憶體中就是一個指針,僅僅擁有指向對象的空間大小。

變數和對象的關係在於引用,變數引用對象後,也就對應了賦值的過程。

在 python 中一切皆為對象,具體在記憶體中表示一塊記憶體空間,每一個對象都會具有 identity,type 和 value 這三個內容。

Identity, 一旦對象被創建後,Identity 的值便不會發生改變。在 Cpython 中,其值體現為記憶體中保存對象的地址。is 操作符,比較對象是否相等就是通過這個值。通過 id() 函數查看它的整數形式。

Type, 和 Identity 一樣,在對象創建後,Type 也不會發生變化。它主要定義了一些可能支持的值和操作(如對列表來說,會有求長度的操作)。通過 type() 函數可以得到對象的類型。

Value,用於表示的某些對象的值。當對象在創建後值可以改變稱為 mutable,否則的話被稱為 immutable.

舉個例子,比如在 C 中,int x = 4 在記憶體中,是先分配了一個 int 類型的記憶體空間,然後把 4 放進空間內。

而 Python 中,x = 4 正好相反,是為 4 分配了一塊的記憶體空間,然後用 x 指向它。由於變數可以指向各種類型的對象,因此不需要像 C 一樣聲明變數。這也就是 Python 被稱為動態類型的意義。

並且在 Python 中,變數可以刪除,但對象是無法刪除的。

2.immutable 和 mutable 對象

immutable 對象擁有一個固定的值,包括 numbers, strings, tuples. 一個新的值被保存時,一個新的對象就會被創建。這些對象在作為常量的 hash 值中有著非常重要的作用,如作為字典的 key 時。

mutable 對象可以改變自身的值,但 id() 並不會發生改變。

當一些對象包含對其他對象的一些引用時,我們稱這些對象為 containers, 例如 list, tuple, dictionary 這些都是 containers. 這裡需要註意的是,一個 immutable containers 可以包含對 mutable 對象的引用(如在 tuple 中包含一個 list)。 但這個對象仍然稱為 immutable 對象,因為 Identity 是不變的。

3.hashable 對象

當一個對象在生命周期內(實現了__hash__()方法)hash 值不會發生改變,並可以與其他對象進行比較(實現了__eq__()方法),稱之為hashable 對象。

在 Python 內置的 immutable 對象 大多數都是 hashable 對象。immutable containers(tuples, frozenset)在引用的對象都是 hashable 對象時,才是hashable 對象。mutable containers 容器都不是 hashable 對象。用戶自定義的類都是 hashable 對象,

4.淺拷貝與深拷貝

在介紹對象的拷貝前,先介紹一下 Python 中的賦值操作,可以讓我們更好的瞭解拷貝的過程。

5.賦值操作

賦值操作的右邊是簡單表達式:

def normal_operation():

    # immutable objects
    # int
    a = 10
    b = 10
    print('----- int')
    print("id of a:{} , id of b: {}".format(id(a), id(b)))
    # id of a:1777364320 , id of b: 1777364320
    print(a == b)  # True
    print(a is b)  # True
    # str
    str_a = '123'
    str_b = '123'
    print('----- str')
    print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
    # id of a:1615046978224 , id of b: 1615046978224
    print(str_a == str_b)  # True
    print(str_a is str_b)  # True

    # tuple
    tuple_a = (1, 2, 3)
    tuple_b = (1, 2, 3)
    print('----- tuple')
    print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
    # id of a:1615047009696 , id of b: 1615047024856
    print(tuple_a == tuple_b)  # True
    print(tuple_a is tuple_b)  # False

    # mutable
    # set
    set_a = {1, 2, 3}
    set_b = {1, 2, 3}
    print('----- set')
    print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
    # id of a:1615045625000 , id of b: 1615047012872
    print(set_a == set_b)  # True
    print(set_a is set_b)  # False

    # list
    list_a = [1, 2, 3]
    list_b = [1, 2, 3]
    print('----- list')
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:1615047017800 , id of b: 1615045537352
    print(list_a == list_b)  # True
    print(list_a is list_b)  # False

    # dict
    dict_a = {"name": "xxx", "age": "123"}
    dict_b = {"name": "xxx", "age": "123"}
    print('----- dict')
    print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
    # id of a:1615045521696 , id of b: 1615045522128
    print(dict_a == dict_b)  # True
    print(dict_a is dict_b)  # False

在 Cpython 中,id() 反映了對象在記憶體中的地址。可以看到,對於 immutable 對象中的 number 和 string 來說,CPython 本身對其做了一定的優化,在創建相同的內容時,使其 指向了相同的記憶體地址,從而被覆用。

但是,Python 不會對所有 mutable 對象執行此操作,因為實現此功能需要一定的運行時成本。對於在記憶體中的對象來說,必須首先在記憶體中搜索對象(搜索意味著時間)。對於 number 和 string 來說,搜索到它們很容易,所以才對其做了這樣的優化。

對於其他類型的對象,雖然創建的內容相同,但都在記憶體中完全創建了一塊新的區域。

6. 賦值操作的右邊是 Python 中已存在的變數:

def assignment_operation():
    # immutable objects
    # int
    a = 10
    b = a
    print('----- int')
    print("id of a:{} , id of b: {}".format(id(a), id(b)))
    # id of a:1777364320 , id of b: 1777364320
    print(a == b)  # True
    print(a is b)  # True
    # str
    str_a = '123'
    str_b = str_a
    print('----- str')
    print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
    # id of a:2676110142128 , id of b: 2676110142128
    print(str_a == str_b)  # True
    print(str_a is str_b)  # True

    # tuple
    tuple_a = (1, 2, 3)
    tuple_b = tuple_a
    print('----- tuple')
    print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
    # id of a:2676110191640 , id of b: 2676110191640
    print(tuple_a == tuple_b)  # True
    print(tuple_a is tuple_b)  # True

    # mutable
    # set
    set_a = {1, 2, 3}
    set_b = set_a
    print('----- set')
    print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
    # id of a:2676108788904 , id of b: 2676108788904
    print(set_a == set_b)  # True
    print(set_a is set_b)  # True

    # list
    list_a = [1, 2, 3]
    list_b = list_a
    print('----- list')
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:2676110181704 , id of b: 2676110181704
    print(list_a == list_b)  # True
    print(list_a is list_b)  # True

    # dict
    dict_a = {"name": "xxx", "age": "123"}
    dict_b = dict_a
    print('----- dict')
    print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
    # id of a:2676079063328 , id of b: 2676079063328
    print(dict_a == dict_b)  # True
    print(dict_a is dict_b)  # True

而當賦值操作的右邊是已經存在的 Python 對象時,不論是什麼類型的對象,都沒有在記憶體中創建新的內容,僅僅是聲明瞭一個新的變數指向之前記憶體中已經創建的對象,就像提供了一個別名一樣。

>>> dict_a = {'1':1}
>>> dict_b = dict_a
>>> print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
id of a:140355639151936 , id of b: 140355639151936

>>> dict_b = {}
>>> print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
id of a:140355639151936 , id of b: 140355639922176

由於 dict_b = dict_a操作,讓兩個變數同時指向了同一塊記憶體區域。自然 id 相等。
當對 dict_b 重新賦值時,僅讓 b 指向了另外一塊記憶體區域,並不會影響 a 的指向,由於兩塊記憶體區域不同,自然id 並不想等。

7.改變賦值後的對象

def assignment_operation_change():
    # immutable objects
    # int
    a = 10
    print("id of a:{}".format(id(a)))  
    # id of a:1994633728
    b = a 
    a = a + 10
    print('----- int')
    print("id of a:{} , id of b: {}".format(id(a), id(b)))
    # id of a:1994634048 , id of b: 1994633728
    print(a == b)  # False
    print(a is b)  # False

    # mutable objects
    # list
    list_a = [1, 2, 3]
    list_b = list_a
    list_a.append(4)
    print('----- list')
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:2676110181704 , id of b: 2676110181704
    print(list_a == list_b)  # True
    print(list_a is list_b)  # True

當修改 imutable 對象時,由於其本身不可改變,只能在記憶體中新申請一塊新的空間,用於存儲修改後的內容。對應上面 a=20 的操作,這時再判斷 a 和 b 時,由於指向了記憶體的不同位置,所以 a,b不在相等。a 原來指向的記憶體區域不會被回收,因為現在由 b 指向。可以看到 b 指向的記憶體地址和 a 之前的指向的記憶體地址是一致的。

當修改 mutable 對象時,由於都指向相同的記憶體地址,所以對變數 list_a 修改的操作,也會映射到變數 list_b。

總結一下:

  • 指向 imutable 的不同變數,當其中一個變數被修改時,其他變數不受影響,因為被修改後的變數會指向一個新創建的對象。

  • 指向 mutable 對象的不同變數,當其中一個變數修改這個對象時,會影響到指向這個對象的所有變數。

8.淺拷貝

淺拷貝創建了一個對象,這個對象包含了對被拷貝元素的參考。 所以當使用淺拷貝來複制 conainters 對象時,僅僅拷貝了那些嵌套元素的引用。

def shallow_copy():
    # immutable objects
    # int
    a = 10
    b = copy(a)
    print('----- int')
    print("id of a:{} , id of b: {}".format(id(a), id(b)))
    # id of a:1777364320 , id of b: 1777364320
    print(a == b)  # True
    print(a is b)  # True
    # str
    str_a = '123'
    str_b = copy(str_a)
    print('----- str')
    print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
    # id of a:2676110142128 , id of b: 2676110142128
    print(str_a == str_b)  # True
    print(str_a is str_b)  # True

    # tuple
    tuple_a = (1, 2, 3)
    # Three methods of shallow copy
    # tuple_b = tuple_a[:]
    # tuple_b = tuple(tuple_a)
    tuple_b = copy(tuple_a)
    print(id(tuple_b))
    print('----- tuple')
    print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
    # id of a:2676110191640 , id of b: 2676110191640
    print(tuple_a == tuple_b)  # True
    print(tuple_a is tuple_b)  # True

    # mutable
    # set
    set_a = {1, 2, 3}
    # Two methods of shallow copy
    # set_b = set(set_a)
    set_b = copy(set_a)
    print('----- set')
    print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
    # id of a:2099885540520 , id of b: 2099888490984
    print(set_a == set_b)  # True
    print(set_a is set_b)  # False

    # list
    list_a = [1, 2, 3]
    # Three methods of shallow copy
    # list_b = list_a[:]
    # list_b = list(list_b)
    list_b = copy(list_a)
    print('----- list')
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:2099888478280 , id of b: 2099888478472
    print(list_a == list_b)  # True
    print(list_a is list_b)  # False
	# Python小白學習交流群:711312441
    # dict
    dict_a = {"name": "xxx", "age": "123"}
    # Two methods of shallow copy
    # dict_b = dict(dict_a)
    dict_b = copy(dict_a)
    print('----- dict')
    print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
    # id of a:2099855880480 , id of b: 2099886881024
    print(dict_a == dict_b)  # True
    print(dict_a is dict_b)  # False

這裡有一點需要註意,對於 string 和 number 來說,正如上面提到的 Cpython 做了相應的優化,讓不同的變數指向了相同的記憶體地址,進而 id 的值是相等的。

但對於元組這個 immutable 元素來說,執行 淺拷貝時,也不會創建一個記憶體區域,只是返回一個老元組的引用。

對於其他的 mutable 對象,在淺拷貝後都會創建一個新的記憶體區域,包含了被拷貝元素的引用。

淺拷貝正如它的名字那樣,當拷貝嵌套的 mutable 元素時,就會出現問題:

def shallow_copy_change_value():
    # list
    list_a = [1, 2, 3, [4, 5, 6]]
    list_b = copy(list_a)
    list_a[0] = 10
    list_a[3].append(7)
    print('----- list')
    print("ia:{} ,b: {}".format(list_a, list_b))
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # a:[10, 2, 3, [4, 5, 6, 7]] ,b: [1, 2, 3, [4, 5, 6, 7]]
    # id of a:1698595158472 , id of b: 1698595159752
    print(list_a == list_b)  # False
    print(list_a is list_b)  # False

下麵是對上面 list 淺拷貝的圖解:

執行淺拷貝操作:

72a62de32295d24a41f15aecb2f65eac

在 list_b 執行淺拷貝後,創建一個新的對象,新對象中的 list_a[0] 指向 1.

修改 list_a 操作:

f82e60e92a6c04ab94146965ee4e985e

當執行 list_a[0] = 10 操作時,由於 list_a[0] 本身是 number 類型,會重新創建一塊區域,用於保存新的值 10. 而新創建的 list_b[0] 並不會受到影響,還會指向之前的記憶體區域。

當修改list_a[3] 操作時,由於list_a[3] 在淺拷貝後,新創建的對象中不會 嵌套創建 一個新的 list_a[3] 對象,僅僅是指向了之前的 list_a[3] 對象。所以當修改 list_a[3] 時, list_b[3] 也會收到影響。

9.深拷貝

對於深拷貝操作來說,除了會創建一個新的對象外,會還遞歸的遍歷老對象的中的嵌套元素,並形成新的副本。

def shallow_deepcopy_change_value():
    # list
    list_a = [1, 2, 3, [4, 5, 6]]
    list_b = deepcopy(list_a)
    list_a[0] = 10
    list_a[3].append(7)
    print('----- list')
    print("a:{} ,b: {}".format(list_a, list_b))
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:2099888478280 , id of b: 2099888478472
    print(list_a == list_b)  # False
    print(list_a is list_b)  # False

下麵是對應圖解過程:

執行深拷貝操作:

4a050f660c31f509ecde59eb8b589c38

修改 list_a 操作:

cd944895e4e8cb41278113baa1906af9

這裡 list_a 和 list_b 已經是完全的不同的兩個對象。

總結

在這篇文章中,主要介紹了 Python 中對象,以及對象的拷貝過程,主要有下麵幾個重要的內容:

  • Python 中變數沒有類型,僅僅可看做一個指針,通過引用指向對象。變數可以刪除,但對象不行。

  • Python 對象被創建後,會擁有 identity,type 和 value 三個屬性。

  • immutable 和 mutable,主要在於 value 在其生命周期內是否能發生變化。

  • 修改 mutable 對象時,所有指向它的變數都會受到影響。修改 immutable 對象時,指向它的其他變數沒有影響。

  • immutable 的大多數對象都是 hashable,但要考慮 immutable containers 的特殊情況。

  • 淺拷貝會創建一個新的記憶體區域(對象),但其內部是對原對象內部引用的拷貝,在使用 mutable 對象時,存在一定的風險。

  • 深拷貝不但會創建一個新的記憶體區域(對象),還會遞歸的創建原對象的所有嵌套對象,但也帶來了一些效率的問題。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 在切換詳情頁中有這麼一個場景,點擊上一條,會顯示上一條的詳情頁,同理,點擊下一條,會顯示下一條的詳情頁。 偽代碼如下所示: 我們定義了一個 switcher 模版, 用戶點擊上一條、下一條時調用 goToPreOrNext 方法。該頁面通 ...
  • 隨著 Web 應用的複雜性不斷增加,性能優化成為了開發人員必須面對的挑戰之一。Vue 路由懶載入是一項關鍵技術,它可以幫助我們提高 Web 應用的載入速度,從而提升用戶體驗。 在本篇技術博文中,我們將深入探討 Vue 路由懶載入的背景、原理以及使用方法。我們還將分享一些優化和進階技巧,幫助開發人員... ...
  • 倉庫地址:https://gitee.com/JSTGitee/element-jst-admin 登錄 首頁 表格 前言 該方案作為一套多功能的後臺框架模板,適用於絕大部分的後臺管理系統開發。基於 Vue2,使用 vue-cli2 腳手架,引用 Element ui 組件庫,方便開發快速簡潔好看的 ...
  • 這是一個講解DDD落地的文章系列,作者是《實現領域驅動設計》的譯者滕雲。本文章系列以一個真實的並已成功上線的軟體項目——碼如雲(https://www.mryqr.com)為例,系統性地講解DDD在落地實施過程中的各種典型實踐,以及在面臨實際業務場景時的諸多取捨。 本系列包含以下文章: DDD入門( ...
  • ## 轉載放在最前 [一文帶你瞭解,虛擬記憶體、記憶體分頁、分段、段頁式記憶體管理](https://zhuanlan.zhihu.com/p/451736494)[[Golang三關-典藏版]一站式Golang記憶體洗髓經 | Go 技術論壇](https://learnku.com/articles/6 ...
  • 垃圾收集器 HotSpot虛擬機包含的所有收集器如圖3-5所示。圖3-5展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。 新生代收集器:Serial、ParNew、Parallel Scavenge,新生代收集器均採用複製演算法 老年代收集器:Serial Old ...
  • 基本環境準備(第一節)2023年8月9日16:37 1.安裝Node.js;Windows 上安裝 Node.js你可以採用以下兩種方式來安裝。1、Windows 安裝包(.msi)本文實例以 v0.10.26 版本為例,其他版本類似, 安裝步驟: 步驟 1 : 雙擊下載後的安裝包 v0.10.26 ...
  • 在一個需要用到flag作為信號控制代碼中一些代碼片段是否運行的,比如"--flag True"或者"--flag False"。 但是古怪的是無法傳入False,無論傳入True還是False,程式裡面都是True的參數,所以這個flag並沒有生效,也就失去了意義。 參考代碼: ```python ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...