python——賦值與深淺拷貝

  初學編程的小夥伴都會對於深淺拷貝的用法有些疑問,今天我們就結合python變量存儲的特性從內存的角度來談一談賦值和深淺拷貝~~~

預備知識一——python的變量及其存儲

  在詳細的瞭解python中賦值、copy和deepcopy之前,我們還是要花一點時間來了解一下python內存中變量的存儲情況。

  在高級語言中,變量是對內存及其地址的抽象。對於python而言,python的一切變量都是對象,變量的存儲,採用了引用語義的方式,存儲的只是一個變量的值所在的內存地址,而不是這個變量的只本身。

引用語義:在python中,變量保存的是對象(值)的引用,我們稱爲引用語義。採用這種方式,變量所需的存儲空間大小一致,因爲變量只是保存了一個引用。也被稱爲對象語義和指針語義。
值語義:有些語言採用的不是這種方式,它們把變量的值直接保存在變量的存儲區裏,這種方式被我們稱爲值語義,例如C語言,採用這種存儲方式,每一個變量在內存中所佔的空間就要根據變量實際的大小而定,無法固定下來。
值語義和引用語義的區別:
值語義:   死的、 傻的、 簡單的、 具體的、 可複製的
引用語義: 活的、 聰明的、 複雜的、 抽象的、 不可複製的

  我們來看一張簡單易懂的圖理解一下python的引用語義和C語言值語義在內存中的存儲情況,左右兩個圖,分別表示了python中變量存儲與C語言中變量存儲區別:

  

預備知識二——各基本數據類型的地址存儲及改變情況

  在python中的數據類型包括:bool、int、long、float、str、set、list、tuple、dict等等。我們可以大致將這些數據類型歸類爲簡單數據類型和複雜的數據結構。

我的劃分標準是,如果一個數據類型,可以將其他的數據類型作爲自己的元素,我就認爲這是一種數據結構。數據結構的分類有很多種,但是在Python中常用的只有集合、序列和映射三種結構。對應python中的set、list(tuple、str)、dict;常用的數據類型有int、long、float、bool、str等類型。(其中,str類型比較特別,因爲從C語言的角度來說,str其實是一個char的集合,但是這與本文的關聯不大,所以我們暫時不談這個問題)

  

  由於python中的變量都是採用的引用語義,數據結構可以包含基礎數據類型,導致了在python中數據的存儲是下圖這種情況,每個變量中都存儲了這個變量的地址,而不是值本身;對於複雜的數據結構來說,裏面的存儲的也只只是每個元素的地址而已。:

  

  1.數據類型重新初始化對python語義引用的影響

  變量的每一次初始化,都開闢了一個新的空間,將新內容的地址賦值給變量。對於下圖來說,我們重複的給str1賦值,其實在內存中的變化如下右圖:

           

  從上圖我們可以看出,str1在重複的初始化過程中,是因爲str1中存儲的元素地址由'hello world'的地址變成了'new hello world'的。

  2.數據結構內部元素變化重對python語義引用的影響

  對於複雜的數據類型來說,改變其內部的值對於變量的影響:

           

  當對列表中的元素進行一些增刪改的操作的時候,是不會影響到lst1列表本身對於整個列表地址的,只會改變其內部元素的地址引用。可是當我們對於一個列表重新初始化(賦值)的時候,就給lst1這個變量重新賦予了一個地址,覆蓋了原本列表的地址,這個時候,lst1列表的內存id就發生了改變。上面這個道理用在所有複雜的數據類型中都是一樣的。

變量的賦值

  搞明白了上面的內容,再來探討變量的賦值,就變得非常簡單了。

  1.str的賦值

             

  我們剛剛已經知道,str1的再次初始化(賦值)會導致內存地址的改變,從上圖的結果我們可以看出修改了str1之後,被賦值的str2從內存地址到值都沒有受到影響。

  看內存中的變化,起始的賦值操作讓str1和str2變量都存儲了‘hello world’所在的地址,重新對str1初始化,使str1中存儲的地址發生了改變,指向了新建的值,此時str2變量存儲的內存地址並未改變,所以不受影響。

  2.複雜的數據結構中的賦值

  剛剛我們看了簡單數據類型的賦值,現在來看複雜數據結構變化對應內存的影響。

           

  上圖對列表的增加修改操作,沒有改變列表的內存地址,lst1和lst2都發生了變化。

  對照內存圖我們不難看出,在列表中添加新值時,列表中又多存儲了一個新元素的地址,而列表本身的地址沒有變化,所以lst1和lst2的id均沒有改變並且都被添加了一個新的元素。

  簡單的比喻一下,我們出去吃飯,lst1和lst2就像是同桌吃飯的兩個人,兩個人公用一張桌子,只要桌子不變,桌子上的菜發生了變化兩個人是共同感受的。

初識拷貝

  我們已經詳細瞭解了變量賦值的過程。對於複雜的數據結構來說,賦值就等於完全共享了資源,一個值的改變會完全被另一個值共享。

  然而有的時候,我們偏偏需要將一份數據的原始內容保留一份,再去處理數據,這個時候使用賦值就不夠明智了。python爲這種需求提供了copy模塊。提供了兩種主要的copy方法,一種是普通的copy,另一種是deepcopy。我們稱前者是淺拷貝,後者爲深拷貝。

  深淺拷貝一直是所有編程語言的重要知識點,下面我們就從內存的角度來分析一下兩者的區別。

淺拷貝

  首先,我們來了解一下淺拷貝。淺拷貝:不管多麼複雜的數據結構,淺拷貝都只會copy一層。下面就讓我們看一張圖,來了解一下淺淺拷貝的概念。

           

      看上面兩張圖,我們加入左圖表示的是一個列表sourcelist,sourcelist = ['str1','str2','str3','str4','str5',['str1','str2','str3','str4','str5']];

  右圖在原有的基礎上多出了一個淺拷貝的copylist,copylist = ['str1','str2','str3','str4','str5',['str1','str2','str3','str4','str5']];

  sourcelist和copylist表面上看起來一模一樣,但是實際上在內存中已經生成了一個新列表,copy了sourceLst,獲得了一個新列表,存儲了5個字符串和一個列表所在內存的地址。

      我們看下面分別對兩個列表進行的操作,紅色的框框裏面是變量初始化,初始化了上面的兩個列表;我們可以分別對這兩個列表進行操作,例如插入一個值,我們會發現什麼呢?如下所示:

         

   從上面的代碼我們可以看出,對於sourceLst和copyLst列表添加一個元素,這兩個列表好像是獨立的一樣都分別發生了變化,但是當我修改lst的時候,這兩個列表都發生了變化,這是爲什麼呢?我們就來看一張內存中的變化圖:

         

  我們可以知道sourceLst和copyLst列表中都存儲了一坨地址,當我們修改了sourceLst1的元素時,相當於用'sourceChange'的地址替換了原來'str1'的地址,所以sourceLst的第一個元素髮生了變化。而copyLst還是存儲了str1的地址,所以copyLst不會發生改變。

  當sourceLst列表發生變化,copyLst中存儲的lst內存地址沒有改變,所以當lst發生改變的時候,sourceLst和copyLst兩個列表就都發生了改變。

  這種情況發生在字典套字典、列表套字典、字典套列表,列表套列表,以及各種複雜數據結構的嵌套中,所以當我們的數據類型很複雜的時候,用copy去進行淺拷貝就要非常小心。。。

深拷貝

  剛剛我們瞭解了淺拷貝的意義,但是在寫程序的時候,我們就是希望複雜的數據結構之間完全copy一份並且它們之間又沒有一毛錢關係,應該怎麼辦呢?

  我們引入一個深拷貝的概念,深拷貝——即python的copy模塊提供的另一個deepcopy方法。深拷貝會完全複製原變量相關的所有數據,在內存中生成一套完全一樣的內容,在這個過程中我們對這兩個變量中的一個進行任意修改都不會影響其他變量。下面我們就來試驗一下。

       

      看上面的執行結果,這一次我們不管是對直接對列表進行操作還是對列表內嵌套的其他數據結構操作,都不會產生拷貝的列表受影響的情況。我們再來看看這些變量在內存中的狀況:

     

  看了上面的內容,我們就知道了深拷貝的原理。其實深拷貝就是在內存中重新開闢一塊空間,不管數據結構多麼複雜,只要遇到可能發生改變的數據類型,就重新開闢一塊內存空間把內容複製下來,直到最後一層,不再有複雜的數據類型,就保持其原引用。這樣,不管數據結構多麼的複雜,數據之間的修改都不會相互影響。這就是深拷貝~~~

原文地址:http://www.cnblogs.com/Eva-J/p/5534037.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章