面试官:Python 对象的垃圾回收策略是什么?
裸睡的猪
共 4611字,需浏览 10分钟
·
2020-12-30 17:28
作者 | wedo实验君
来源 | Python中文社区
1. 引言
值传递
和引用传递
。python的对象引用也是学习python过程中需要特别关注的一个知识点,特别是对函数参数传递,可能会引起不必要的BUG。本文将对引用做一个梳理,内容涉及如下:变量和赋值 可变对象和不可变对象 函数参数的引用 浅拷贝和深拷贝 垃圾回收 弱引用
2. python引用
2.1 变量和赋值
is
或者比较id()
的判断是否引用的是同一个内存地址的变量。==
是比较两个对象的内容是否相等,即两个对象的值是否相等is
同时检查对象的值和内存地址。可以通过is判断是否是同一个对象id()
列出变量的内存地址的编号
# 这个例子a 和 b 两个变量共同指向了同一个内存空间
a = [1, 2, 3]
c = [1, 2, 3]
print(a is c) # False
print(a == c) # True
b = a
a.append(5)
print(a is b) # True
初始化赋值:变量的每一次初始化,都开辟了一个新的空间,将新内容的地址赋值给变量
变量赋值:引用地址的传递
2.2 可变对象和不可变对象
可变对象包括字典dict、列表list、集合set、手动声明的类对象等 不可变对象包括数字int float、字符str、None、元组tuple等
list 可变对象,内容变更地址不变 a = [1, 2, 3]
print(id(a))
a.append(5)
print(id(a))不可变对象(常用的共享地址或缓存) # 较小整数频繁被使用,python采用共享地址方式来管理
a = 1
b = 1
print(a is b) # True
# 对于单词类str,python采用共享缓存的方式来共享地址
a = 'hello'
b = 'hello'
print(a is b) # True不可变对象(不共享地址) a = (1999, 1)
b = (1999, 1)
print(a is b) # False
a = 'hello everyone'
b = 'hello everyone'
print(a is b) # False元组的相对不可变型
# 元组的里元素是可变,改变可变的元素,不改变元组的引用
a = (1999, [1, 2])
ida = id(a)
a[-1].append(3)
idb = id(a)
print(ida == idb) # True
a = [1, 2, 3]
print(id(a))
a = a + [5]
print(id(a))
# 前后两个变量a, 已经不是同一个地址了
2.3 函数参数的引用
def func(d):
d['a'] = 10
d['b'] = 20 # 改变了外部实参的值
d = {'a': 0, 'b': 1} # 赋值操作, 局部d贴向了新的标识
print(d) # {'a': 0, 'b': 1}
d = {}
func(d)
print(d) # {'a': 10, 'b': 20}
class bus():
def __init__(self, param=[]):
self.param = param
def test(self, elem):
self.param.append(elem)
b = bus([2, 3])
b.param # [2, 3]
c = bus()
c.test(3)
c.param # [3]
d = bus()
d.param # [3] # c 中修改了默认值的引用的内容
2.4 浅拷贝和深拷贝
浅拷贝:只复制顶层的对象,对于有嵌套数据结构,内部的元素还是原有对象的引用,这时候需要特别注意 深拷贝:复制了所有对象,递归式的复制所有对象。复制后的对象和原来的对象是完全不同的对象。对于不可变对象来说,浅拷贝和深拷贝都是一样的地址。但是对于嵌套了可变对象元素的情况,就有所不同
test_a = (1, 2, 3)
test_b = copy.copy(test_a)
test_c = copy.deepcopy(test_a)
print(test_a is test_b) # True
print(test_a is test_c) # True
test_a[2].append(5) # 改变不可变对象中可变元素的内容
print(test_a is test_b) # True
print(test_a is test_c) # False
print(test_c) # (1, 2, [3, 4])
l1 = [3, [66, 55, 44], (2, 3, 4)]
l2 = list(l1) # l2是l1的浅拷贝
# 顶层改变不会相互影响,因为是两个不同对象
l1.append(50)
print(l1) # 3, [66, 55, 44], (2, 3, 4), 50]
print(l2) # [3, [66, 55, 44], (2, 3, 4)]
# 嵌套可变元素,浅拷贝共享一个地址
l1[1].append(100)
print(l1) # [3, [66, 55, 44, 100], (2, 3, 4), 50]
print(l2) # [3, [66, 55, 44, 100], (2, 3, 4)]
# 嵌套不可变元素,不可变元素的操作是创建一个新的对象,所以不影响
l1[2] += (2,3)
print(l1) # [3, [66, 55, 44, 100], (2, 3, 4, 2, 3), 50]
print(l2) #[3, [66, 55, 44, 100], (2, 3, 4)]
2.5 垃圾回收
引用计数:python可以给所有的对象(内存中的区域)维护一个引用计数的属性,在一个引用被创建或复制的时候,让python,把相关对象的引用计数+1;相反当引用被销毁的时候就把相关对象的引用计数-1。当对象的引用计数减到0时,认为整个python中不会再有变量引用这个对象,所以就可以把这个对象所占据的内存空间释放出来了。可以通过 sys.getrefcount()
来查看对象的引用分代回收: 分代回收主要是为了提高垃圾回收的效率。对象的创建和消费的频率不一样。由于python在垃圾回收前需要检测是否是垃圾,是否回收,然后再回收。当对象很多的时候,垃圾检测的耗时变得很大,效率很低。python采用的对对象进行分代,按不同的代进行不同的频率的检测。代等级的规则根据对象的生命时间来判断,比如一个对象连续几次检测都是可达的,这个对象代的等级高,降低检测频率。python中默认把所有对象分成三代。第0代包含了最新的对象,第2代则是最早的一些对象 循环引用:一个对象直接或者间接引用自己本身,引用链形成一个环。这样改对象的引用计数永远不可能为0。所有能够引用其他对象的对象都被称为容器(container). 循环引用只能发生容器之间发生. Python的垃圾回收机制利用了这个特点来寻找需要被释放的对象。
import sys
a = [1, 2]
b = a
print(sys.getrefcount(a)) # 3 命令本身也是一次引用
del b
print(sys.getrefcount(a)) # 2
3. 弱引用
应用在缓存中,只存在一定的时间存在。当它引用的对象存在时,则对象可用,当对象不存在时,就返回None 不增加引用计数,在循环引用使用,就降低内存泄露的可能性
import weakref
a_set = {0,1}
wref = weakref.ref(a_set) # 建立弱引用
print(wref()) # {0,1}
a_set = {2, 3, 4} # 原来的a_set 引用计数为0,垃圾回收
print(wref()) # None # 所值对象被垃圾回收, 弱引用也消失为None
4. 总结
对象赋值就完成引用,变量是地址引用式的变量 要时刻注意,所以引用可变对象对象的改变,是否导致共同引用的变量值得变化 函数会修改是可变对象的实参 浅拷贝只是copy顶层,如果存在内部嵌套可变对象,要注意,copy的还是引用 对象的引用计数为0时,就开始垃圾回收 弱引用为增加引用计数,与被所指对象共存亡,而不影响循环引用
评论