l2 是 l1 的浅拷贝,它们指向不同的对象,因为浅拷贝里的元素是对原对象元素的引用,因此 l2 中的元素和 l1 指向同一个列表和元组对象(l1[0]和l2[0]指向的是相同的地址) 。l1.append(6)不会对 l2 产生任何影响,因为 l2 和 l1 作为整体是两个不同的对象,不共享内存地址 。
l1[0].append(3)对 l1 中的第一个列表新增元素 3,因为 l2 是 l1 的浅拷贝,l2 中的第一个元素和 l1 中的第一个元素,共同指向同一个列表,因此 l2 中的第一个列表也会相对应的新增元素 3 。
这里提一个小问题:如果对l1中的元组新增元素(l1[1] += (7, 8)),会影响l2吗?
到这里我们知道使用浅拷贝可能带来的副作用,要避免它就得使用深度拷贝 。
深度拷贝深度拷贝会完整地拷贝一个对象,会重新分配一块内存,创建一个新的对象,并且将原对象中的元素以递归的方式,通过创建新的子对象拷贝到新对象中 。因此,新对象和原对象没有任何关联,也就是完全拷贝了父对象及其子对象 。
import copyl1 = [[1, 2], (4, 5)]l2 = copy.deepcopy(l1)print(id(l1))print(id(l2))l1.append(6)print(l1)print(l2)l1[0].append(3)print(l1)print(l2)
执行结果:
30260883422803026088342472[[1, 2], (4, 5), 6][[1, 2], (4, 5)][[1, 2, 3], (4, 5), 6][[1, 2], (4, 5)]
可以看到,l1 变化不影响l2 ,l1 和 l2 完全独立,没有任何联系 。
在进行深度拷贝时,深度拷贝 deepcopy 中会维护一个字典,记录已经拷贝的对象与其 ID 。如果字典里已经存储了将要拷贝的对象,则会从字典直接返回 。
Python垃圾回收Python垃圾回收包括引用计数、标记清除和分代回收
引用计数引用计数是一种垃圾收集机制,当一个python对象被引用时,引用计数加 1,当一个对象的引用为0时,该对象会被当做垃圾回收 。
from sys import getrefcountl1 = [1, 2, 3]print(getrefcount(l1)) # 查看引用计数l2 = l1print(getrefcount(l2))
执行结果:
23
在使用 getrefcount()的时候,变量作为参数传进去,会多一次引用 。
del语句会删除对象的一个引用 。请看下面的例子
from sys import getrefcountclass TestObjectA():def __init__(self):print("hello!!!")def __del__(self):print("bye!!!")a = TestObjectA()b = ac = aprint(getrefcount(c))del aprint(getrefcount(c))del bprint(getrefcount(c))del cprint("666")
执行结果:
hello!!!432bye!!!666
方法__del__ 的作用是当对象被销毁时调用 。其中del a删除了变量a,但是对象TestObjectA仍然存在,它还被b和c引用,所以不会被回收,引用计数为0时会被回收 。上面的例子中,将a,b,c都删除后引用的对象被回收(打印“666”之前) 。
另外重新赋值也会删除对象的一个引用 。
标记清除如果出现了循环引用,引用计数方法就无法回收,导致内存泄漏 。先来看下面的例子:
class TestObjectA(dict):def __init__(self):print("A: hello!!!")def __del__(self):print("A: bye!!!")class TestObjectB(dict):def __init__(self):print("B: hello!!!")def __del__(self):print("B: bye!!!")a = TestObjectA()b = TestObjectB()a['1'] = bb['1'] = adel adel bprint("666")
执行结果:
A: hello!!!B: hello!!!666A: bye!!!B: bye!!!
上面的代码存在循环引用,删除a和b之后,它们的引用计数还是1,仍然大于0,不会被回收(打印“666”之后) 。
标记清除可解决循环引用问题,从根对象(寄存器和程序栈上的引用)出发,遍历对象,将遍历到的对象打上标记(垃圾检测),然后在内存中清除没有标记的对象(垃圾回收) 。上面的例子中,a和b相互引用,如果与其他对象没有引用关系就不会遍历到它,也就不会被标记,所以会被清除 。
分代回收如果频繁进行标记清除会影响Python性能,有很多对象,清理了很多次他依然存在,可以认为,这样的对象不需要经常回收,也就是说,对象存在时间越长,越可能不是垃圾 。
将回收对象进行分代(一共三代),每代回收的时间间隔不同,其中新创建的对象为0代,如果一个对象能在第0代的垃圾回收过程中存活下来,那么它就被放入到1代中,如果1代里的对象在第1代的垃圾回收过程中存活下来,则会进入到2代 。
gc模块以下三种情况会启动垃圾回收:
- 调用gc.collect():强制对所有代执行一次回收
- 当gc模块的计数器达到阀值的时候 。
- 程序退出的时候