Last Updated on

前言

我们知道,在面向对象编程时,变量与对象的时间内容是存在一个指针关系的。也就是实际的内容储存在内存中,变量则指向此内存地址。

Python 作为一门动态语言,其不需要预先定义变量类型,可以直接通过=进行赋值,但是有时候 = 赋值是会存在一些问题的。Python中有三种方法可以实现一个对象到另一个对象的复制。

  • = 赋值
  • 浅copy
  • 深copy

这些方式由于实现方式的不同,就会有一些问题和差异,需要特别注意。下面我就单独详细将一下这3种方式。

= 赋值

= 赋值,会赋值对象的内存地址引用,并不会复制内容。如下:

a = 'A'
b = a

print(id(a))
print(id(b))

# 输出结果:
4435309768
4435309768

这就会导致,如果内存中的数据发生类变化,那么变量也就会发生变化,我们知道Python中的对象,分为可变对象和不可变对象。对于不可变对象,无法修改,改变时,只会新创建新的内容,所以不影响, 但是对于可变变量,则需要注意:

看下面示例:

a = [1]
b = a

a.append(2)

print(a)
print(b)
print(id(a))
print(id(b))

如上所以,a改变后,b也跟着改变了。Python中的可变对象有列表,字典,集合。当需要这类对象的 = 赋值时,需要注意。

Python中,除了这些数据类型外,函数,类也是对象,并且是平时我们最常见的对象,同样也可以使用 = 赋值。

对于函数的赋值,看下面示例:

def a():
    print('a')


b = a

def a():
    print('aa')

a()
b()
print(id(a))
print(id(b))

# 输出结果:
aa
a
4357507544
4355276312

如上可以看到,函数a,赋值给了b后,再重新定义,会重新创建新的函数内容的内存,与原来的函数已经不一样了,内存地址发生改变,就不会互相影响到。

但是,函数中也是有对象函数的属性的,如果改变了函数的属性,那么实际上就是修改了内存中的内容,所以修改也会影响指向同一内存地址的变量。

看下面示例:

def a():
    print('a')

b = a
a.__name__ = 'new_name'


print(a.__name__)
print(b.__name__)
print(id(a))
print(id(b))

# 输出结果:
new_name
new_name
4343492120
4343492120

如上是函数的赋值,那么对于类呢?

类和函数也是类似的,当类的属性和方法改变时,类的内存地址并没有改变,那么修改依然会影响到指向相同内存地址的变量。

看下面示例:

class A(object):
    name = 'A'

    def work(self):
        print('A is working')

def work2(self):
    print('B is working')

B = A

A.name = 'new_name'
A.work = work2
print(A.name)
print(B.name)
a = A()
b = B()
print(id(a))
print(id(b))
a.work()
b.work()
print(id(A))
print(id(B))


# 输出结果:
new_name
new_name
4356473800
4356473856
B is working
B is working
140510332221112
140510332221112

如上所示,可以看出,当类A的属性和方法改变后,类B的属性和方法也相应的发生了变化,因为其指向同一个内存地址,所以实际上类B就是类A。创建的类示例a和b则是内存地址不一样,所以实际上是不同的对象,虽然具有相同的属性和方法。但是如果修改a的属性,b的属性是不会发生变化的。


所以,理解=赋值,就是要明白,其只是赋值了对内存地址的引用。只要内存地址的数据发生变化,对应的变量都将发生变化。

浅copy

因为=赋值存在的问题,当我们需要将对象内容暂存,不想有如上的一些改变影响。那么就需要用到copy()方法,copy方法又分为深copy和浅copy。

copy模块中的copy方法默认就是浅copy。 看下面示例:

import copy

a = [1,2,3, [4, 5]]
b = copy.copy(a)

a.append(6)
print(a)
print(b)

print(id(a))
print(id(b))
print(id(a[3]))
print(id(b[3]))

a[3].append(6)
print(a)
print(b)


# 输出结果:
[1, 2, 3, [4, 5], 6]
[1, 2, 3, [4, 5]]
4367396616
4367405128
4367159816
4367159816
[1, 2, 3, [4, 5, 6], 6]
[1, 2, 3, [4, 5, 6]]

如上所示,浅copy会将赋值一份外层的数据,并创建新的内存地址,所以,对不同的内存地址做的操作,如在列表a中新增数据,也只是会修改列表a,并不会影响到列表b。

但是你会发现,如果我们修改列表a中的内嵌列表a[3],那么修改后,列表b也改变了。为什么呢? 请看下面示例:

import copy

a = [1,2,3, [4, 5]]
b = copy.copy(a)

for i in range(4):
    if id(a[i]) == id(b[i]):
        print(True)

# 输出结果:
True
True
True
True

看到了吗,虽然浅copy创建了新的内存地址,但是在列表中的元素,却还是保留着原来的内存地址,所以其实其列表内部,并没有完全创建新的数据,还是用的原来的内存地址,也就是相当于使用 = 赋值了引用而已。

所以这就又回到了原来赋值引用的问题,当列表中有可变对象时,修改可变对象后,改变自然就跟随这内存地址,影响到了浅copy出来的列表b。

对于其他可变对象,dict,set,浅copy的原理是一样的。要理解清楚,浅copy只拷贝了第一层的数据。内层则还是拷贝的引用。

深copy

深copy则是完全拷贝数据,创建一份完整的新的相同的数据。存放在不同的内存地址中。

看如下示例:

import copy

a = [1, (2,3), [4, 5]]
b = copy.deepcopy(a)

for i in range(3):
    if id(a[i]) == id(b[i]):
        print(True)
    else:
        print(a[i])


# 输出结果:
True
True
[4, 5]

发现了什么吗,输出了3个True,也就是列表前面的元素的内存地址并没有改变,只有可变对象的内存地址发生了改变,创建了新的数据。

按我们正常的思维,因为是列表里每个元素的内存都发生改变才对啊。为什么呢?这就需要了解Python底层的内存机制了。这个目前我也还不是完全明了,以后完全搞明白了,再单独拿出来跟大家分享。

但是其中所有可变的对象,都已经对值进行了拷贝,进过深copy的两个变量,已经可以说是完全不相关了。

总结

要理解Python的这几种赋值copy方式,主要就是理解值和引用的区别,内存的变化。了解之后,就知道在什么情况下该用什么。

有任何问题,欢迎留言交流