Python的比較與拷貝

我們在前面已經接觸到了很多Python對象比較的例子,例如這樣的

a = 123
b = 123
a == b

或者是將一個對象進行拷貝

l1 = [1,2,3,4,5]
l2 = l1
l3 = list(l1)

那麼現在試一下下面的代碼:先創建個列表l1,再把這個列表進行一份拷貝至l2,最後把l1添加一個元素,看l2會發生什麼變化?

'''
遇到問題沒人解答?小編創建了一個Python學習交流QQ羣:579817333 
尋找有志同道合的小夥伴,互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書!
'''
>>> l1 = [1,2,3,4,5]
>>> l2 = l1
>>> l1.append(6)
>>> l2
[1, 2, 3, 4, 5, 6]

是不是l2也變了!這裏就引申出來一個概念:淺拷貝(shallow copy)和深拷貝(deep copy)

在對拷貝的概念進行分析前,我們先看一看下面的知識:

‘is’VS‘==’

is和==是我們在進行對象比較的時候常用的方法,簡單的來說:

== 操作是用來比較兩個對象的值是否相等,比如下面的例子,就表示了變量a和b所指向的值是否相等

>>> a = 123
>>> b = 123
>>> a == b
True

而is操作是用來判定對象的身份標識是否是相等的,也就是說判定兩個對象是否指向同一內存地址。

在python中,每個對象都有一個ID,我們可以通過函數id()來獲得

>>>a = 123
>>>id(a)
2011987232

因此,is操作就是判定兩個對象的id是否相等,我們可以看一看下面的操作

>>> a = 10
>>> b = 10
>>> id(a)
>>> id(b)
>>> a is b
True

過程是這樣的:Python會爲10這個整形值開闢一塊內存,然後變量a和b都指向這塊內存區域,所以a和b的id是一樣的。但特別要注意的一點:這種情況只適用於-5到256範圍內的整形數據。比如下面的例子

'''
遇到問題沒人解答?小編創建了一個Python學習交流QQ羣:579817333 
尋找有志同道合的小夥伴,互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書!
'''
>>> a = 100
>>> b = 100
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False

處於對性能優化的考慮,Python內部會對-5到256的整型維持一個數組而起到一個緩存的作用。這樣每次調用這些數的時候Python就會從這個數組中返回相對應的引用,而不是重新開闢一塊內存空間。但是如果超出了這個範圍,Python則會爲這個數開闢兩塊不同的區域,所以a和b的id就不一樣了。

通常我們的實際工作中用==的次數會比is多得多,因爲我們一般更關心兩個變量的值而不是他們的存儲地址。但是當我們比較一個變量和一個單例(singleton)時,我們常常用is,一個典型的例子就是和None比較

if a is None:
    pass
if a is not None:
    pass

這裏我們要強調一下代碼的效率,兩種比較操作符中,is是比==高的,因爲is操作符不能被重載,這樣Python就不需要尋找程序中是否有其他的地方重載了比較操作符,直接去比較兩個變量的ID就可以了。

但是==的操作相當於去執行了a.__ eq__(b)這個函數,而Python大部分的數據都會去重載__eq__()這個函數,比如對於列表, __eq__函數回去遍歷列表終端元素,比較他們的順序和值是否相等。

說句題外話,對於不可變(immutable)的變量,如果我們之前比較過,是不是就一直不變了呢?答案是否定的,我們看下面的例子

'''
遇到問題沒人解答?小編創建了一個Python學習交流QQ羣:579817333 
尋找有志同道合的小夥伴,互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書!
'''
>>> t1 = (1,2,[3,4])
>>> t2 = (1,2,[3,4])
>>> t1 == t2
True
>>> t1[-1].append(5)
>>> t1 == t2
False

元組是不可變的,但是元組可以嵌套使用,這樣我們就可以修改元組中的某個元素,那麼元組本身就改變了)

淺拷貝和深拷貝

接下來我們就看看Python中的淺拷貝和深拷貝

對於這兩個操作,我們先看一看淺拷貝,最常用的淺拷貝的方法,是使用數據類型本身的構造器:

>>> l1 = [1,2,3]
>>> l2 = list(l1)
>>>
>>> l2
[1, 2, 3]
>>> l1 == l2
True
>>> l1 is l2
False

這裏,l2就是l1的淺拷貝,對於可變的序列,我們還可以通過切片操作完成淺拷貝

'''
遇到問題沒人解答?小編創建了一個Python學習交流QQ羣:579817333 
尋找有志同道合的小夥伴,互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書!
'''
>>> l1 =  [1,2,3,4,5]
>>> l2 = l1[:]
>>> l2 == l1
True
>>> l2 is l1
False

我們還可以提供相對應的函數進行淺拷貝

import copy
l1 = [1,2,3,4,5]
l2 = copy.copy(l1)

但是這裏有非常重要的一點特別的情況:對於元組,使用tuple()或者切片操作是不會創建一份淺拷貝,反而會返回一個指向相同元組的引用

>>> t1 = (1,2,3,4,5)
>>> t2 = tuple(t1)
>>> t1 == t2
True
>>> t1 is t2
True

元組(1,2,3,4,5)只被創建了一次,t1和t2同時指向這個元組。

所以,淺拷貝是指重新分配一塊內存,創建一個新的對象,裏面的元素是對源對象中子對象的引用,如果原對象中的元素不可變,倒無所謂,但如果元素可變,淺拷貝會帶來一些副作用,尤其需要注意,我們看看下面的例子:

'''
遇到問題沒人解答?小編創建了一個Python學習交流QQ羣:579817333 
尋找有志同道合的小夥伴,互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書!
'''
>>> l1 = [[1,2],(30,40)]
>>> l2 = list(l1)
>>> l1.append(100)
>>> l1[0].append(3)
>>> l1
[[1, 2, 3], (30, 40), 100]
>>>
>>> l2
[[1, 2, 3], (30, 40)]
>>>
>>> l1[1] += (50,60)
>>> l1
[[1, 2, 3], (30, 40, 50, 60), 100]
>>>
>>> l2
[[1, 2, 3], (30, 40)]

在上面的例子中,我們先定義了個列表l1,裏面有一個列表還有一個元組,然後我們把l1淺拷貝出來一個l2.因爲淺拷貝里的元素是對原對象元素的引用,因此l2和l1指向同一個元組和列表對象。接着對l1新添加一個對象100,這個操作是不會對l2產生影響的,因爲l2和l1作爲整體是兩個不同的對象,並不共享內存地址。操作過後l2不變,l1會發生改變。

然後在把l1[0]裏添加一個元素3,同樣因爲l2是l1的淺拷貝,l2中第一個元素和l1中的第一個元素指向同一個列表,因此l2中的第一個列表也會相對應的新增元素3,也就是說l1和l2都會改變。

最後是l1[1] += (50,60),因爲元組是不可變的,這裏表示l1中的元組進行拼接,然後重新創建了一個新元組作爲l1中索引爲1的元素。而l2沒有重新將新元組進行引用,所以l2不受影響。操作後l2不變l1變化。

從上面的例子我們發現如果在拷貝中使用淺拷貝可能帶來的副作用,所以爲了避免這種副作用我們可以使用深度拷貝來完整的拷貝一個對象

>>> import copy
>>> l1 = [[1,2],(30,40)]
[[1, 2], (30, 40)]
>>> l2 = copy.deepcopy(l1)
>>> l1,append(100)
>>> l1[0].append(3)
>>> l1
[[1, 2, 3], (30, 40), 100]
>>> l2
[[1, 2], (30, 40)]

可以看出來無論l1怎麼變化,l2都不會變化,因爲l1和l2相對來說是完全獨立沒有任何聯繫的。

但是深度拷貝有些時候也會帶來一些問題,如果被拷貝對象啊中存在指向自身的引用,呢麼程序就會陷入無限循環

'''
遇到問題沒人解答?小編創建了一個Python學習交流QQ羣:579817333 
尋找有志同道合的小夥伴,互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書!
'''
>>> import copy
>>> x = [1]
>>> x.append(x)
>>> x
[1, [...]]
>>> y = copy.deepcopy(x)
>>> y
[1, [...]]

看上面的例子,列表x中有指向自身的引用,所以x是一個無限嵌套的列表,但是我們發現深度拷貝x到y以後,程序中並沒出現stack overflow的現象,是因爲deepcopy中會維護一個字典用來記錄已經拷貝的對象與其ID,在拷貝中如果字典裏已經存儲了要拷貝的對象,則會從字典直接返回,我們可以看看deepcopy對應的代碼

'''
遇到問題沒人解答?小編創建了一個Python學習交流QQ羣:579817333 
尋找有志同道合的小夥伴,互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書!
'''
def deepcopy(x, memo=None, _nil=[]):
    """Deep copy operation on arbitrary Python objects.
      
  See the module's __doc__ string for more info.
  """
  
    if memo is None:
        memo = {}
    d = id(x) # 查詢被拷貝對象x的id
  y = memo.get(d, _nil) # 查詢字典裏是否已經存儲了該對象
  if y is not _nil:
      return y # 如果字典裏已經存儲了將要拷貝的對象,則直接返回
        ...

總結

1.比較操作符’=='表示比較對象間的值是否相等,而‘is’表示對象的ID是否相等,及他們是否指向同一塊內存地址

2.比較操作符"is"效率由於 " == " , 因爲is無法被重載,只是簡單的獲取對象的ID並進行比較;而"=="操作會遞歸的遍歷對象的所有值,並逐一進行對比

3.淺拷貝中的元素是原對象中子對象的引用,因此如果原對象中的元素是可變的,將其改變後可能影響拷貝後的對象,存在一定的副作用

4.深度拷貝會遞歸的拷貝原對象中每一個子對象,因此拷貝後的對象和原對象互相獨立不影響。此外深度拷貝會維護一個字典用來記錄已經拷貝的對象及其ID,可用來提高效率並防止無限遞歸的發生。

最後留一個思考題,下面的代碼輸入時什麼?爲什麼?

import copy
x = [1]
x.append(x)

y = copy.deepcopy(x)
#下面的輸出是什麼?
x==y

因爲x和y是個無限嵌套的列表,在用“==”進行比較是會進行遞歸比較,遍歷列表中的所有值,而python中爲了防止棧崩潰,限制了遞歸的層數,最後就會爆出錯誤

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