溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Python的對象拷貝和內(nèi)存布局如何實現(xiàn)

發(fā)布時間:2023-05-16 11:29:10 來源:億速云 閱讀:93 作者:iii 欄目:編程語言

今天小編給大家分享一下Python的對象拷貝和內(nèi)存布局如何實現(xiàn)的相關(guān)知識點,內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

    前言

    你知道下面一些程序片段的輸出結(jié)果嗎?

    a = [1, 2, 3, 4]
    b = a
    print(f"{a = } \t|\t {b = }")
    a[0] = 100
    print(f"{a = } \t|\t {b = }")
    a = [1, 2, 3, 4]
    b = a.copy()
    print(f"{a = } \t|\t {b = }")
    a[0] = 100
    print(f"{a = } \t|\t {b = }")
    a = [[1, 2, 3], 2, 3, 4]
    b = a.copy()
    print(f"{a = } \t|\t {b = }")
    a[0][0] = 100
    print(f"{a = } \t|\t {b = }")
    a = [[1, 2, 3], 2, 3, 4]
    b = copy.copy(a)
    print(f"{a = } \t|\t {b = }")
    a[0][0] = 100
    print(f"{a = } \t|\t {b = }")
    a = [[1, 2, 3], 2, 3, 4]
    b = copy.deepcopy(a)
    print(f"{a = } \t|\t {b = }")
    a[0][0] = 100
    print(f"{a = } \t|\t {b = }")

    Python 對象的內(nèi)存布局

    在 python 當(dāng)中我們應(yīng)該如何確定一個對象的內(nèi)存地址呢?在 Python 當(dāng)中給我們提供了一個內(nèi)嵌函數(shù) id() 用于得到一個對象的內(nèi)存地址:

    a = [1, 2, 3, 4]
    b = a
    print(f"{a = } \t|\t {b = }")
    a[0] = 100
    print(f"{a = } \t|\t {b = }")
    print(f"{id(a) = } \t|\t {id(b) = }")
    # 輸出結(jié)果
    # a = [1, 2, 3, 4]  |  b = [1, 2, 3, 4]
    # a = [100, 2, 3, 4]  |  b = [100, 2, 3, 4]
    # id(a) = 4393578112  |  id(b) = 4393578112

    事實上上面的對象內(nèi)存布局是有一點問題的,或者說是不夠準(zhǔn)確的,但是也是能夠表示出各個對象之間的關(guān)系的,我們現(xiàn)在來深入了解一下。在 Cpython 里你可以認(rèn)為每一個變量都可以認(rèn)為是一個指針,指向被表示的那個數(shù)據(jù),這個指針保存的就是這個 Python 對象的內(nèi)存地址。

    在 Python 當(dāng)中,實際上列表保存的指向各個 Python 對象的指針,而不是實際的數(shù)據(jù),因此上面的一小段代碼,可以用如下的圖表示對象在內(nèi)存當(dāng)中的布局:

    Python的對象拷貝和內(nèi)存布局如何實現(xiàn)

    變量 a 指向內(nèi)存當(dāng)中的列表 [1, 2, 3, 4],列表當(dāng)中有 4 個數(shù)據(jù),這四個數(shù)據(jù)都是指針,而這四個指針指向內(nèi)存當(dāng)中 1,2,3,4 這四個數(shù)據(jù)??赡苣銜幸蓡枺@不是有問題嗎?都是整型數(shù)據(jù)為什么不直接在列表當(dāng)中存放整型數(shù)據(jù),為啥還要加一個指針,再指向這個數(shù)據(jù)呢?

    事實上在 Python 當(dāng)中,列表當(dāng)中能夠存放任何 Python 對象,比如下面的程序是合法的:

    data = [1, {1:2, 3:4}, {'a', 1, 2, 25.0}, (1, 2, 3), "hello world"]

    在上面的列表當(dāng)中第一個到最后一個數(shù)據(jù)的數(shù)據(jù)類型為:整型數(shù)據(jù),字典,集合,元祖,字符串,現(xiàn)在來看為了實現(xiàn)  Python 的這個特性,指針的特性是不是符合要求呢?每個指針?biāo)加玫膬?nèi)存是一樣的,因此可以使用一個數(shù)組去存儲 Python 對象的指針,然后再將這個指針指向真正的 Python 對象!

    牛刀小試

    在經(jīng)過上面的分析之后,我們來看一下下面的代碼,他的內(nèi)存布局是什么情況:

    data = [[1, 2, 3], 4, 5, 6]
    data_assign = data
    data_copy = data.copy()

    Python的對象拷貝和內(nèi)存布局如何實現(xiàn)

    Python的對象拷貝和內(nèi)存布局如何實現(xiàn)

    • data_assign = data,關(guān)于這個賦值語句的內(nèi)存布局我們在之前已經(jīng)談到過了,不過我們也再復(fù)習(xí)一下,這個賦值語句的含義就是 data_assign 和 data 指向的數(shù)據(jù)是同一個數(shù)據(jù),也就是同一個列表。

    • data_copy = data.copy(),這條賦值語句的含義是將 data 指向的數(shù)據(jù)進(jìn)行淺拷貝,然后讓 data_copy 指向拷貝之后的數(shù)據(jù),這里的淺拷貝的意思就是,對列表當(dāng)中的每一個指針進(jìn)行拷貝,而不對列表當(dāng)中指針指向的數(shù)據(jù)進(jìn)行拷貝。從上面的對象的內(nèi)存布局圖我們可以看到 data_copy 指向一個新的列表,但是列表當(dāng)中的指針指向的數(shù)據(jù)和 data 列表當(dāng)中的指針指向的數(shù)據(jù)是一樣的,其中 data_copy 使用綠色的箭頭進(jìn)行表示,data 使用黑色的箭頭進(jìn)行表示。

    查看對象的內(nèi)存地址

    在前面的文章當(dāng)中我們主要分析了一下對象的內(nèi)存布局,在本小節(jié)我們使用 python 給我們提供一個非常有效的工具去驗證這一點。在 python 當(dāng)中我們可以使用 id() 去查看對象的內(nèi)存地址,id(a) 就是查看對象 a 所指向的對象的內(nèi)存地址。

    看下面的程序的輸出結(jié)果:

    a = [1, 2, 3]
    b = a
    print(f"{id(a) = } {id(b) = }")
    for i in range(len(a)):
        print(f"{i = } {id(a[i]) = } {id(b[i]) = }")

    根據(jù)我們之前的分析,a 和 b 指向的同一塊內(nèi)存,也就說兩個變量指向的是同一個 Python 對象,因此上面的多有輸出的 id 結(jié)果 a 和 b 都是相同的,上面的輸出結(jié)果如下:

    id(a) = 4392953984 id(b) = 4392953984
    i = 0 id(a[i]) = 4312613104 id(b[i]) = 4312613104
    i = 1 id(a[i]) = 4312613136 id(b[i]) = 4312613136
    i = 2 id(a[i]) = 4312613168 id(b[i]) = 4312613168

    看一下淺拷貝的內(nèi)存地址:

    a = [[1, 2, 3], 4, 5]
    b = a.copy()
    print(f"{id(a) = } {id(b) = }")
    for i in range(len(a)):
        print(f"{i = } {id(a[i]) = } {id(b[i]) = }")

    根據(jù)我們在前面的分析,調(diào)用列表本身的 copy 方法是對列表進(jìn)行淺拷貝,只拷貝列表的指針數(shù)據(jù),并不拷貝列表當(dāng)中指針指向的真正的數(shù)據(jù),因此如果我們對列表當(dāng)中的數(shù)據(jù)進(jìn)行遍歷得到指向的對象的地址的話,列表 a 和列表 b 返回的結(jié)果是一樣的,但是和上一個例子不同的是 a 和 b 指向的列表的本身的地址是不一樣的(因為進(jìn)行了數(shù)據(jù)拷貝,可以參照下面淺拷貝的結(jié)果進(jìn)行理解)。

    Python的對象拷貝和內(nèi)存布局如何實現(xiàn)

    可以結(jié)合下面的輸出結(jié)果和上面的文字進(jìn)行理解:

    id(a) = 4392953984 id(b) = 4393050112 # 兩個對象的輸出結(jié)果不相等
    i = 0 id(a[i]) = 4393045632 id(b[i]) = 4393045632 # 指向的是同一個內(nèi)存對象因此內(nèi)存地址相等 下同
    i = 1 id(a[i]) = 4312613200 id(b[i]) = 4312613200
    i = 2 id(a[i]) = 4312613232 id(b[i]) = 4312613232

    copy模塊

    在 python 里面有一個自帶的包 copy ,主要是用于對象的拷貝,在這個模塊當(dāng)中主要有兩個方法 copy.copy(x) 和 copy.deepcopy()。

    copy.copy(x) 方法主要是用于淺拷貝,這個方法的含義對于列表來說和列表本身的 x.copy() 方法的意義是一樣的,都是進(jìn)行淺拷貝。這個方法會構(gòu)造一個新的 python 對象并且會將對象 x 當(dāng)中所有的數(shù)據(jù)引用(指針)拷貝一份。

    Python的對象拷貝和內(nèi)存布局如何實現(xiàn)

    copy.deepcopy(x)  這個方法主要是對對象 x 進(jìn)行深拷貝,這里的深拷貝的含義是會構(gòu)造一個新的對象,會遞歸的查看對象 x 當(dāng)中的每一個對象,如果遞歸查看的對象是一個不可變對象將不會進(jìn)行拷貝,如果查看到的對象是可變對象的話,將重新開辟一塊內(nèi)存空間,將原來的在對象 x 當(dāng)中的數(shù)據(jù)拷貝的新的內(nèi)存當(dāng)中。(關(guān)于可變和不可變對象我們將在下一個小節(jié)仔細(xì)分析)

    根據(jù)上面的分析我們可以知道深拷貝的花費(fèi)是比淺拷貝多的,尤其是當(dāng)一個對象當(dāng)中有很多子對象的時候,會花費(fèi)很多時間和內(nèi)存空間。

    對于 python 對象來說進(jìn)行深拷貝和淺拷貝的區(qū)別主要在于復(fù)合對象(對象當(dāng)中有子對象,比如說列表,元祖、類的實例等等)。這一點主要是和下一小節(jié)的可變和不可變對象有關(guān)系。

    可變和不可變對象與對象拷貝

    在 python 當(dāng)中主要有兩大類對象,可變對象和不可變對象,所謂可變對象就是對象的內(nèi)容可以發(fā)生改變,不可變對象就是對象的內(nèi)容不能夠發(fā)生改變。

    • 可變對象:比如說列表(list),字典(dict),集合(set),字節(jié)數(shù)組(bytearray),類的實例對象。

    • 不可變對象:整型(int),浮點型(float),復(fù)數(shù)(complex),字符串,元祖(tuple),不可變集合(frozenset),字節(jié)(bytes)。

    看到這里你可能會有疑問了,整數(shù)和字符串不是可以修改嗎?

    a = 10
    a = 100
    a = "hello"
    a = "world"

    比如下面的代碼是正確的,并不會發(fā)生錯誤,但是事實上其實 a 指向的對象是發(fā)生了變化的,第一個對象指向整型或者字符串的時候,如果重新賦一個新的不同的整數(shù)或者字符串對象的話,python 會創(chuàng)建一個新的對象,我們可以使用下面的代碼進(jìn)行驗證:

    a = 10
    print(f"{id(a) = }")
    a = 100
    print(f"{id(a) = }")
    a = "hello"
    print(f"{id(a) = }")
    a = "world"
    print(f"{id(a) = }")

    上面的程序的輸出結(jié)果如下所示:

    id(a) = 4365566480
    id(a) = 4365569360
    id(a) = 4424109232
    id(a) = 4616350128

    可以看到的是當(dāng)重新賦值之后變量指向的內(nèi)存對象是發(fā)生了變化的(因為內(nèi)存地址發(fā)生了變化),這就是不可變對象,雖然可以對變量重新賦值,但是得到的是一個新對象并不是在原來的對象上進(jìn)行修改的!

    我們現(xiàn)在來看一下可變對象列表發(fā)生修改之后內(nèi)存地址是怎么發(fā)生變化的:

    data = []
    print(f"{id(data) = }")
    data.append(1)
    print(f"{id(data) = }")
    data.append(1)
    print(f"{id(data) = }")
    data.append(1)
    print(f"{id(data) = }")
    data.append(1)
    print(f"{id(data) = }")

    上面的代碼輸出結(jié)果如下所示:

    id(data) = 4614905664
    id(data) = 4614905664
    id(data) = 4614905664
    id(data) = 4614905664
    id(data) = 4614905664

    從上面的輸出結(jié)果來看可以知道,當(dāng)我們往列表當(dāng)中加入新的數(shù)據(jù)之后(修改了列表),列表本身的地址并沒有發(fā)生變化,這就是可變對象。

    我們在前面談到了深拷貝和淺拷貝,我們現(xiàn)在來分析一下下面的代碼:

    data = [1, 2, 3]
    data_copy = copy.copy(data)
    data_deep = copy.deepcopy(data)
    print(f"{id(data ) = } | {id(data_copy) = } | {id(data_deep) = }")
    print(f"{id(data[0]) = } | {id(data_copy[0]) = } | {id(data_deep[0]) = }")
    print(f"{id(data[1]) = } | {id(data_copy[1]) = } | {id(data_deep[1]) = }")
    print(f"{id(data[2]) = } | {id(data_copy[2]) = } | {id(data_deep[2]) = }")

    上面的代碼輸出結(jié)果如下所示:

    id(data ) = 4620333952 | id(data_copy) = 4619860736 | id(data_deep) = 4621137024
    id(data[0]) = 4365566192 | id(data_copy[0]) = 4365566192 | id(data_deep[0]) = 4365566192
    id(data[1]) = 4365566224 | id(data_copy[1]) = 4365566224 | id(data_deep[1]) = 4365566224
    id(data[2]) = 4365566256 | id(data_copy[2]) = 4365566256 | id(data_deep[2]) = 4365566256

    看到這里你肯定會非常疑惑,為什么深拷貝和淺拷貝指向的內(nèi)存對象是一樣的呢?前列我們可以理解,因為淺拷貝拷貝的是引用,因此他們指向的對象是同一個,但是為什么深拷貝之后指向的內(nèi)存對象和淺拷貝也是一樣的呢?這正是因為列表當(dāng)中的數(shù)據(jù)是整型數(shù)據(jù),他是一個不可變對象,如果對 data 或者 data_copy 指向的對象進(jìn)行修改,那么將會指向一個新的對象并不會直接修改原來的對象,因此對于不可變對象其實是不用開辟一塊新的內(nèi)存空間在重新賦值的,因為這塊內(nèi)存中的對象是不會發(fā)生改變的。

    我們再來看一個可拷貝的對象:

    data = [[1], [2], [3]]
    data_copy = copy.copy(data)
    data_deep = copy.deepcopy(data)
    print(f"{id(data ) = } | {id(data_copy) = } | {id(data_deep) = }")
    print(f"{id(data[0]) = } | {id(data_copy[0]) = } | {id(data_deep[0]) = }")
    print(f"{id(data[1]) = } | {id(data_copy[1]) = } | {id(data_deep[1]) = }")
    print(f"{id(data[2]) = } | {id(data_copy[2]) = } | {id(data_deep[2]) = }")

    上面的代碼輸出結(jié)果如下所示:

    id(data ) = 4619403712 | id(data_copy) = 4617239424 | id(data_deep) = 4620032640
    id(data[0]) = 4620112640 | id(data_copy[0]) = 4620112640 | id(data_deep[0]) = 4620333952
    id(data[1]) = 4619848128 | id(data_copy[1]) = 4619848128 | id(data_deep[1]) = 4621272448
    id(data[2]) = 4620473280 | id(data_copy[2]) = 4620473280 | id(data_deep[2]) = 4621275840

    從上面程序的輸出結(jié)果我們可以看到,當(dāng)列表當(dāng)中保存的是一個可變對象的時候,如果我們進(jìn)行深拷貝將創(chuàng)建一個全新的對象(深拷貝的對象內(nèi)存地址和淺拷貝的不一樣)。

    代碼片段分析

    經(jīng)過上面的學(xué)習(xí)對于在本篇文章開頭提出的問題對于你來說應(yīng)該是很簡單的,我們現(xiàn)在來分析一下這幾個代碼片段:

    a = [1, 2, 3, 4]
    b = a
    print(f"{a = } \t|\t {b = }")
    a[0] = 100
    print(f"{a = } \t|\t {b = }")

    這個很簡單啦,a 和 b 不同的變量指向同一個列表,a 中間的數(shù)據(jù)發(fā)生變化,那么 b 的數(shù)據(jù)也會發(fā)生變化,輸出結(jié)果如下所示:

    a = [1, 2, 3, 4]  |  b = [1, 2, 3, 4]
    a = [100, 2, 3, 4]  |  b = [100, 2, 3, 4]
    id(a) = 4614458816  |  id(b) = 4614458816

    我們再來看一下第二個代碼片段

    a = [1, 2, 3, 4]
    b = a.copy()
    print(f"{a = } \t|\t {b = }")
    a[0] = 100
    print(f"{a = } \t|\t {b = }")

    因為 b 是 a 的一個淺拷貝,所以 a 和 b 指向的是不同的列表,但是列表當(dāng)中數(shù)據(jù)的指向是相同的,但是由于整型數(shù)據(jù)是不可變數(shù)據(jù),當(dāng)a[0] 發(fā)生變化的時候,并不會修改原來的數(shù)據(jù),而是會在內(nèi)存當(dāng)中創(chuàng)建一個新的整型數(shù)據(jù),因此列表 b 的內(nèi)容并不會發(fā)生變化。因此上面的代碼輸出結(jié)果如下所示:

    a = [1, 2, 3, 4]  |  b = [1, 2, 3, 4]
    a = [100, 2, 3, 4]  |  b = [1, 2, 3, 4]

    再來看一下第三個片段:

    a = [[1, 2, 3], 2, 3, 4]
    b = a.copy()
    print(f"{a = } \t|\t {b = }")
    a[0][0] = 100
    print(f"{a = } \t|\t {b = }")

    這個和第二個片段的分析是相似的,但是 a[0] 是一個可變對象,因此進(jìn)行數(shù)據(jù)修改的時候,a[0] 的指向沒有發(fā)生變化,因此 a 修改的內(nèi)容會影響 b。

    a = [[1, 2, 3], 2, 3, 4]  |  b = [[1, 2, 3], 2, 3, 4]
    a = [[100, 2, 3], 2, 3, 4]  |  b = [[100, 2, 3], 2, 3, 4]

    最后一個片段:

    a = [[1, 2, 3], 2, 3, 4]
    b = copy.deepcopy(a)
    print(f"{a = } \t|\t {b = }")
    a[0][0] = 100
    print(f"{a = } \t|\t {b = }")

    深拷貝會在內(nèi)存當(dāng)中重新創(chuàng)建一個和a[0]相同的對象,并且讓 b[0] 指向這個對象,因此修改 a[0],并不會影響 b[0],因此輸出結(jié)果如下所示:

    a = [[1, 2, 3], 2, 3, 4]  |  b = [[1, 2, 3], 2, 3, 4]
    a = [[100, 2, 3], 2, 3, 4]  |  b = [[1, 2, 3], 2, 3, 4]

    撕開 Python 對象的神秘面紗

    我們現(xiàn)在簡要看一下 Cpython 是如何實現(xiàn) list 數(shù)據(jù)結(jié)構(gòu)的,在 list 當(dāng)中到底定義了一些什么東西:

    typedef struct {
        PyObject_VAR_HEAD
        /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
        PyObject **ob_item;
    
        /* ob_item contains space for 'allocated' elements.  The number
         * currently in use is ob_size.
         * Invariants:
         *     0 <= ob_size <= allocated
         *     len(list) == ob_size
         *     ob_item == NULL implies ob_size == allocated == 0
         * list.sort() temporarily sets allocated to -1 to detect mutations.
         *
         * Items must normally not be NULL, except during construction when
         * the list is not yet visible outside the function that builds it.
         */
        Py_ssize_t allocated;
    } PyListObject;

    在上面定義的結(jié)構(gòu)體當(dāng)中 :

    allocated 表示分配的內(nèi)存空間的數(shù)量,也就是能夠存儲指針的數(shù)量,當(dāng)所有的空間用完之后需要再次申請內(nèi)存空間。

    ob_item 指向內(nèi)存當(dāng)中真正存儲指向 python 對象指針的數(shù)組,比如說我們想得到列表當(dāng)中第一個對象的指針的話就是 list->ob_item[0],如果要得到真正的數(shù)據(jù)的話就是 *(list->ob_item[0])。

    PyObject_VAR_HEAD 是一個宏,會在結(jié)構(gòu)體當(dāng)中定一個子結(jié)構(gòu)體,這個子結(jié)構(gòu)體的定義如下:

    typedef struct {
        PyObject ob_base;
        Py_ssize_t ob_size; /* Number of items in variable part */
    } PyVarObject;

    這里我們不去談對象 PyObject 了,主要說一下 ob_size,他表示列表當(dāng)中存儲了多少個數(shù)據(jù),這個和 allocated 不一樣,allocated 表示 ob_item 指向的數(shù)組一共有多少個空間,ob_size 表示這個數(shù)組存儲了多少個數(shù)據(jù) ob_size <= allocated。

    在了解列表的結(jié)構(gòu)體之后我們現(xiàn)在應(yīng)該能夠理解之前的內(nèi)存布局了,所有的列表并不存儲真正的數(shù)據(jù)而是存儲指向這些數(shù)據(jù)的指針。

    以上就是“Python的對象拷貝和內(nèi)存布局如何實現(xiàn)”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注億速云行業(yè)資訊頻道。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI