【python】面试高频:浅拷贝 vs 深拷贝、'==' vs 'is'

610 阅读5分钟

关于python在面试中会被问到哪些知识点,其实这主要取决于面试官。

额,别拿刀先,马上扯正题。

从我遇到的问题当中来看,除了有少数的常见问题外,更多的还是平时需要你额外拓展学习了解的知识点:

  • 常见问题:比如,可变/不可变数据类型、json的序列化和反序列化、对象的引用/作用域,字典的常用操作等等。
  • 额外知识:比如,python的内存管理机制、浅拷贝 vs 深拷贝、'==' vs 'is'等等。

面试官为什么会问一些额外知识,我觉得还是在于考察候选人对语言了解的深度如何。如果你能静下心来去看一些python语言的书籍(我是属于没静下心的),回答这些问题基本上都不是什么问题。

我现在也在翻阅几年前就买来的python书籍,有类似这种值得分享的知识点,在后续会一一带来。

今天就先聊聊,我觉得面试出现频率最高的:浅拷贝 vs 深拷贝、'==' vs 'is'

一、'==' vs 'is'

为什么先聊这个,因为在后面的拷贝知识点中,会用到对象的比较。

首先,先抛出知识点:

  • '=='操作符比较对象之间的是否相等。
  • 'is'操作符比较的是对象的身份标识是否相等,即它们是否是同一个对象,是否指向同一个内存地址

注意

  • ==操作符是用来比较对象之间的是否相等。

看示例(命令行交互下运行python):

λ python
>>> a = 10
>>> b = 10
>>> a == b
True
>>> id(a)
2407440149072
>>> id(b)
2407440149072
>>> a is b
True
>>>
  • 可以看到上面有2个变量 a 和 b,它们的值都是 10。
  • 在 Python 中,每个对象的身份标识,都能通过函数 id(object) 获得,所以 使用is,相当于比较对象之间的 ID 是否相等。

在交互运行的结果中,可以看到:

  • a == b,结果是 True。
  • a is b,结果也是 True。

嗯?那这俩效果不是一样嘛?

分析一下:

  1. 首先 Python 会为 10 这个值开辟一块内存。
  2. 变量 a 和 b 同时指向这块内存区域。

所以,因此 a 和 b 的值相等,id 也相等。

但是,对于整型数字,a is bTrue,只适用于 -5 到 256 范围内的数字。
现在用 257 试下:

>>> a = 257
>>> b = 257
>>> a is b
False

看到结果为 False。

原因在于,Python 出于对性能优化的考虑,内部会对 -5 到 256 的整型维持一个数组,起到一个缓存的作用。当你每次试图创建一个 -5 到 256 范围内的整型数字时,Python 都会从这个数组中返回相对应的引用,而不是重新开辟一块新的内存空间。

但是,如果整型数字超过了这个范围,比如上述例子中的 257,Python 则会为两个 257 开辟两块内存区域,因此 a 和 b 的 ID 不一样,a is b就会返回 False 了。

通常情况
我们在使用中,还是用==更多,因为我们更关心两个变量的值,而不是它们内部的存储地址。

不过当判断这个变量是不是为None的时候,通常会使用is了:

if a is None:
    ...

if a is not None:
    ...

二、浅拷贝 vs 深拷贝

深浅先不说,拷贝大家应该都会用到过。

>>> a = [1, 2, 3]
>>> b = list(a)
>>> b
[1, 2, 3]
>>> a == b
True
>>> a is b
False

这里的 b 就是 a 的浅拷贝了,当然你也可以用列表的切片来处理:b = a[:]

还可以用python 内部提供的函数copy来处理。

>>> import copy
>>> a = [1, 2, 3]
>>> b = copy.copy(a)
>>> b
[1, 2, 3]
>>> a is b
False

1. 浅拷贝

浅拷贝:是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。

怎么讲?

来看一个新的示例:

>>> import copy
>>> a = [1, 2, 3, 4, ['a', 'b']]
>>> b = copy.copy(a)
>>> a
[1, 2, 3, 4, ['a', 'b']]
>>> b
[1, 2, 3, 4, ['a', 'b']]
>>> a is b
False
  1. a 是一个列表,列表里面又存放了 5 个元素,其中第5个元素也是一个列表 ['a', 'b']。
  2. b 是 a 的浅拷贝,所以打印出来 a 和 b 的值都相等。
  3. a is b 是 False,说明是2个不同的对象。

注意
“新的对象,里面的元素是原对象中子对象的引用”,这里的子对象的引用,是怎么回事?

现在我去修改 a,增加一个元素,应该是不会影响到 b。

>>> a.append(5)
>>> a
[1, 2, 3, 4, ['a', 'b'], 5]
>>> b
[1, 2, 3, 4, ['a', 'b']]

但是,如果我去修改列表 a 中的子对象 ['a', 'b'],增加一个元素 'c',再去看下 a,b 分别是什么:

>>> a[4].append('c')
>>> a
[1, 2, 3, 4, ['a', 'b', 'c'], 5]
>>> b
[1, 2, 3, 4, ['a', 'b', 'c']]

可以看到,b 受到影响了,b 的子对象列表也从原来的 ['a', 'b'] 变成了现在的 ['a', 'b', 'c']。

所以这也就解释了上述的,“浅拷贝,是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用”

这也是浅拷贝的问题所在,在日常使用过程中,需要注意的地方。

2. 深拷贝

看到这,聪明的你或许已经明白深拷贝的作用了。没错,就是解决了浅拷贝的问题,实现真正的全拷贝。

深拷贝:是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。

因此,新对象和原对象没有任何关联

再重新演示下上面的示例,用深拷贝copy.deepcopy()

>>> import copy
>>> a = [1, 2, 3, 4, ['a', 'b']]
>>> b = copy.deepcopy(a)
>>> a
[1, 2, 3, 4, ['a', 'b']]
>>> b
[1, 2, 3, 4, ['a', 'b']]
>>> a[4].append('c')
>>> a
[1, 2, 3, 4, ['a', 'b', 'c']]
>>> b
[1, 2, 3, 4, ['a', 'b']]

用了深拷贝后,a 就算修改了子对象,b 也没有受到影响了。