Python核心技术与实战——十二|Python的比较与拷贝

  我们在前面已经接触到了很多Python对象比较的例子,例如这样的

a = 123
b = 123
a == b

或者是将一个对象进行拷贝

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

那么现在试一下下面的代码:先创建个列表l1,再把这个列表进行一份拷贝至l2,最后把l1添加一个元素,看l2会发生什么变化?

>>> 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)
2011983616
>>> id(b)
2011983616
>>> a is b
True

过程是这样的:Python会为10这个整形值开辟一块内存,然后变量a和b都指向这块内存区域,所以a和b的id是一样的。但特别要注意的一点:这种情况只适用于-5到256范围内的整形数据。比如下面的例子

>>> 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)的变量,如果我们之前比较过,是不是就一直不变了呢?答案是否定的,我们看下面的例子

>>> 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的浅拷贝,对于可变的序列,我们还可以通过切片操作完成浅拷贝

>>> 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同时指向这个元组。

所以,浅拷贝是指重新分配一块内存,创建一个新的对象,里面的元素是对源对象中子对象的引用,如果原对象中的元素不可变,倒无所谓,但如果元素可变,浅拷贝会带来一些副作用,尤其需要注意,我们看看下面的例子:

>>> 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相对来说是完全独立没有任何联系的。

但是深度拷贝有些时候也会带来一些问题,如果被拷贝对象啊中存在指向自身的引用,呢么程序就会陷入无限循环

>>> import copy
>>> x = [1]
>>> x.append(x)
>>> x
[1, [...]]
>>> y = copy.deepcopy(x)
>>> y
[1, [...]]

看上面的例子,列表x中有指向自身的引用,所以x是一个无限嵌套的列表,但是我们发现深度拷贝x到y以后,程序中并没出现stack overflow的现象,是因为deepcopy中会维护一个字典用来记录已经拷贝的对象与其ID,在拷贝中如果字典里已经存储了要拷贝的对象,则会从字典直接返回,我们可以看看deepcopy对应的代码

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



猜你喜欢

转载自www.cnblogs.com/yinsedeyinse/p/11719543.html