網站首頁 編程語言 正文
前言
在本篇文章當中主要給大家介紹 python 當中的拷貝問題,話不多說我們直接看代碼,你知道下面一些程序片段的輸出結果嗎?
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 對象的內存布局
在 python 當中我們應該如何確定一個對象的內存地址呢?在 Python 當中給我們提供了一個內嵌函數 id() 用于得到一個對象的內存地址:
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)?=?}") #?輸出結果 #?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
事實上上面的對象內存布局是有一點問題的,或者說是不夠準確的,但是也是能夠表示出各個對象之間的關系的,我們現在來深入了解一下。在 Cpython 里你可以認為每一個變量都可以認為是一個指針,指向被表示的那個數據,這個指針保存的就是這個 Python 對象的內存地址。
在 Python 當中,實際上列表保存的指向各個 Python 對象的指針,而不是實際的數據,因此上面的一小段代碼,可以用如下的圖表示對象在內存當中的布局:
變量 a 指向內存當中的列表?[1, 2, 3, 4]
,列表當中有 4 個數據,這四個數據都是指針,而這四個指針指向內存當中 1,2,3,4 這四個數據??赡苣銜幸蓡枺@不是有問題嗎?都是整型數據為什么不直接在列表當中存放整型數據,為啥還要加一個指針,再指向這個數據呢?
事實上在 Python 當中,列表當中能夠存放任何 Python 對象,比如下面的程序是合法的:
data?=?[1,?{1:2,?3:4},?{'a',?1,?2,?25.0},?(1,?2,?3),?"hello?world"]
在上面的列表當中第一個到最后一個數據的數據類型為:整型數據,字典,集合,元祖,字符串,現在來看為了實現 ?Python 的這個特性,指針的特性是不是符合要求呢?每個指針所占用的內存是一樣的,因此可以使用一個數組去存儲 Python 對象的指針,然后再將這個指針指向真正的 Python 對象!
牛刀小試
在經過上面的分析之后,我們來看一下下面的代碼,他的內存布局是什么情況:
data?=?[[1,?2,?3],?4,?5,?6] data_assign?=?data data_copy?=?data.copy()
-
data_assign = data
,關于這個賦值語句的內存布局我們在之前已經談到過了,不過我們也再復習一下,這個賦值語句的含義就是 data_assign 和 data 指向的數據是同一個數據,也就是同一個列表。 -
data_copy = data.copy()
,這條賦值語句的含義是將 data 指向的數據進行淺拷貝,然后讓 data_copy 指向拷貝之后的數據,這里的淺拷貝的意思就是,對列表當中的每一個指針進行拷貝,而不對列表當中指針指向的數據進行拷貝。從上面的對象的內存布局圖我們可以看到 data_copy 指向一個新的列表,但是列表當中的指針指向的數據和 data 列表當中的指針指向的數據是一樣的,其中 data_copy 使用綠色的箭頭進行表示,data 使用黑色的箭頭進行表示。
查看對象的內存地址
在前面的文章當中我們主要分析了一下對象的內存布局,在本小節我們使用 python 給我們提供一個非常有效的工具去驗證這一點。在 python 當中我們可以使用 id() 去查看對象的內存地址,id(a) 就是查看對象 a 所指向的對象的內存地址。
看下面的程序的輸出結果:
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])?=?}")
根據我們之前的分析,a 和 b 指向的同一塊內存,也就說兩個變量指向的是同一個 Python 對象,因此上面的多有輸出的 id 結果 a 和 b 都是相同的,上面的輸出結果如下:
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
看一下淺拷貝的內存地址:
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])?=?}")
根據我們在前面的分析,調用列表本身的 copy 方法是對列表進行淺拷貝,只拷貝列表的指針數據,并不拷貝列表當中指針指向的真正的數據,因此如果我們對列表當中的數據進行遍歷得到指向的對象的地址的話,列表 a 和列表 b 返回的結果是一樣的,但是和上一個例子不同的是 a 和 b 指向的列表的本身的地址是不一樣的(因為進行了數據拷貝,可以參照下面淺拷貝的結果進行理解)。
可以結合下面的輸出結果和上面的文字進行理解:
id(a)?=?4392953984?id(b)?=?4393050112?#?兩個對象的輸出結果不相等 i?=?0?id(a[i])?=?4393045632?id(b[i])?=?4393045632?#?指向的是同一個內存對象因此內存地址相等?下同 i?=?1?id(a[i])?=?4312613200?id(b[i])?=?4312613200 i?=?2?id(a[i])?=?4312613232?id(b[i])?=?4312613232
copy模塊
在 python 里面有一個自帶的包 copy ,主要是用于對象的拷貝,在這個模塊當中主要有兩個方法 copy.copy(x) 和 copy.deepcopy()。
copy.copy(x) 方法主要是用于淺拷貝,這個方法的含義對于列表來說和列表本身的 x.copy() 方法的意義是一樣的,都是進行淺拷貝。這個方法會構造一個新的 python 對象并且會將對象 x 當中所有的數據引用(指針)拷貝一份。
copy.deepcopy(x) ?這個方法主要是對對象 x 進行深拷貝,這里的深拷貝的含義是會構造一個新的對象,會遞歸的查看對象 x 當中的每一個對象,如果遞歸查看的對象是一個不可變對象將不會進行拷貝,如果查看到的對象是可變對象的話,將重新開辟一塊內存空間,將原來的在對象 x 當中的數據拷貝的新的內存當中。(關于可變和不可變對象我們將在下一個小節仔細分析)
根據上面的分析我們可以知道深拷貝的花費是比淺拷貝多的,尤其是當一個對象當中有很多子對象的時候,會花費很多時間和內存空間。
對于 python 對象來說進行深拷貝和淺拷貝的區別主要在于復合對象(對象當中有子對象,比如說列表,元祖、類的實例等等)。這一點主要是和下一小節的可變和不可變對象有關系。
可變和不可變對象與對象拷貝
在 python 當中主要有兩大類對象,可變對象和不可變對象,所謂可變對象就是對象的內容可以發生改變,不可變對象就是對象的內容不能夠發生改變。
- 可變對象:比如說列表(list),字典(dict),集合(set),字節數組(bytearray),類的實例對象。
- 不可變對象:整型(int),浮點型(float),復數(complex),字符串,元祖(tuple),不可變集合(frozenset),字節(bytes)。
看到這里你可能會有疑問了,整數和字符串不是可以修改嗎?
a?=?10 a?=?100 a?=?"hello" a?=?"world"
比如下面的代碼是正確的,并不會發生錯誤,但是事實上其實 a 指向的對象是發生了變化的,第一個對象指向整型或者字符串的時候,如果重新賦一個新的不同的整數或者字符串對象的話,python 會創建一個新的對象,我們可以使用下面的代碼進行驗證:
a?=?10 print(f"{id(a)?=?}") a?=?100 print(f"{id(a)?=?}") a?=?"hello" print(f"{id(a)?=?}") a?=?"world" print(f"{id(a)?=?}")
上面的程序的輸出結果如下所示:
id(a)?=?4365566480
id(a)?=?4365569360
id(a)?=?4424109232
id(a)?=?4616350128
可以看到的是當重新賦值之后變量指向的內存對象是發生了變化的(因為內存地址發生了變化),這就是不可變對象,雖然可以對變量重新賦值,但是得到的是一個新對象并不是在原來的對象上進行修改的!
我們現在來看一下可變對象列表發生修改之后內存地址是怎么發生變化的:
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)?=?}")
上面的代碼輸出結果如下所示:
id(data)?=?4614905664
id(data)?=?4614905664
id(data)?=?4614905664
id(data)?=?4614905664
id(data)?=?4614905664
從上面的輸出結果來看可以知道,當我們往列表當中加入新的數據之后(修改了列表),列表本身的地址并沒有發生變化,這就是可變對象。
我們在前面談到了深拷貝和淺拷貝,我們現在來分析一下下面的代碼:
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])?=?}")
上面的代碼輸出結果如下所示:
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
看到這里你肯定會非常疑惑,為什么深拷貝和淺拷貝指向的內存對象是一樣的呢?前列我們可以理解,因為淺拷貝拷貝的是引用,因此他們指向的對象是同一個,但是為什么深拷貝之后指向的內存對象和淺拷貝也是一樣的呢?這正是因為列表當中的數據是整型數據,他是一個不可變對象,如果對 data 或者 data_copy 指向的對象進行修改,那么將會指向一個新的對象并不會直接修改原來的對象,因此對于不可變對象其實是不用開辟一塊新的內存空間在重新賦值的,因為這塊內存中的對象是不會發生改變的。
我們再來看一個可拷貝的對象:
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])?=?}")
上面的代碼輸出結果如下所示:
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
從上面程序的輸出結果我們可以看到,當列表當中保存的是一個可變對象的時候,如果我們進行深拷貝將創建一個全新的對象(深拷貝的對象內存地址和淺拷貝的不一樣)。
代碼片段分析
經過上面的學習對于在本篇文章開頭提出的問題對于你來說應該是很簡單的,我們現在來分析一下這幾個代碼片段:
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 中間的數據發生變化,那么 b 的數據也會發生變化,輸出結果如下所示:
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 指向的是不同的列表,但是列表當中數據的指向是相同的,但是由于整型數據是不可變數據,當a[0] 發生變化的時候,并不會修改原來的數據,而是會在內存當中創建一個新的整型數據,因此列表 b 的內容并不會發生變化。因此上面的代碼輸出結果如下所示:
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] 是一個可變對象,因此進行數據修改的時候,a[0] 的指向沒有發生變化,因此 a 修改的內容會影響 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?=?}")
深拷貝會在內存當中重新創建一個和a[0]相同的對象,并且讓 b[0] 指向這個對象,因此修改 a[0],并不會影響 b[0],因此輸出結果如下所示:
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 對象的神秘面紗
我們現在簡要看一下 Cpython 是如何實現 list 數據結構的,在 list 當中到底定義了一些什么東西:
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;
在上面定義的結構體當中 :
allocated 表示分配的內存空間的數量,也就是能夠存儲指針的數量,當所有的空間用完之后需要再次申請內存空間。
ob_item 指向內存當中真正存儲指向 python 對象指針的數組,比如說我們想得到列表當中第一個對象的指針的話就是 list->ob_item[0],如果要得到真正的數據的話就是 *(list->ob_item[0])。
PyObject_VAR_HEAD 是一個宏,會在結構體當中定一個子結構體,這個子結構體的定義如下:
typedef?struct?{ ????PyObject?ob_base; ????Py_ssize_t?ob_size;?/*?Number?of?items?in?variable?part?*/ }?PyVarObject;
這里我們不去談對象 PyObject 了,主要說一下 ob_size,他表示列表當中存儲了多少個數據,這個和 allocated 不一樣,allocated 表示 ob_item 指向的數組一共有多少個空間,ob_size 表示這個數組存儲了多少個數據 ob_size <= allocated。
在了解列表的結構體之后我們現在應該能夠理解之前的內存布局了,所有的列表并不存儲真正的數據而是存儲指向這些數據的指針。
總結
在本篇文章當中主要給大家介紹了 python 當中對象的拷貝和內存布局,以及對對象內存地址的驗證,最后稍微介紹了一下 cpython 內部實現列表的結構體,幫助大家深入理解列表對象的內存布局。
原文鏈接:https://mp.weixin.qq.com/s/gJitB8C03AIQo5w0viYkdg
相關推薦
- 2022-07-17 android?studio實現簡單的計算器小功能_Android
- 2022-09-29 C語言開發實現通訊錄管理系統_C 語言
- 2022-10-24 React中父子組件通信詳解_React
- 2022-04-26 jquery實現拖拽table表頭改變列寬_jquery
- 2022-05-20 如何保證緩存和數據庫的一致性?
- 2022-04-02 Python字符串與正則表達式詳細介紹_python
- 2023-07-25 使用線程池異步執行定時任務
- 2023-03-23 Android進階CoordinatorLayout協調者布局實現吸頂效果_Android
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支