python对象引用和垃圾回收
Posted Sakura
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python对象引用和垃圾回收相关的知识,希望对你有一定的参考价值。
变量="标签"
变量a和变量b引用同一个列表:
>>> a = [1, 2, 3] >>> b = a >>> a.append(4) >>> b [1, 2, 3, 4]
使用"标签"很形象的解释了变量 =========> 列表[1, 2, 3]是一个物品,而a和b都是给这个物品贴上的标签。因此,改变a的内容,b的内容也改变了。
"is"和"=="
有一个人叫做李华,1997年生,身体情况工作信息记录为info,有个小名叫"小华"。
>>> lihua = {‘name‘:‘lihua‘,‘born‘:‘1997‘,‘information‘:‘info‘} >>> xiaohua = lihua >>> xiaohua is lihua True >>> id(xiaohua),id(lihua) (2072419437304, 2072419437304) >>> xiaohua[‘information‘] = ‘new_info‘ >>> lihua {‘name‘: ‘lihua‘, ‘born‘: ‘1997‘, ‘information‘: ‘new_info‘}
可见xiaohua和lihua指代同一个对象,假如有个冒充者(李华)说他是李华,身份信息一模一样,记为anony。
>>> anony = {‘name‘: ‘lihua‘, ‘born‘: ‘1997‘, ‘information‘: ‘new_info‘} >>> anony == lihua True >>> anony is lihua False
此时使用"is"和"=="判断结果是不同的。lihua和xiaohua绑定同一个对象,xiaohua是lihua的别名;而lihua和anony绑定不同对象。
"=="比较的是对象的值,而"is"比较对象的标识。
在Python中,对象的标识就是id()函数返回值,而is比较的就是这个返回值的整数表示。在Cpython中,id()返回的是对象的内存地址,在其他Python解释器中可能是别的值。最主要的是,id()函数返回值在对象的生命周期中一定不会改变。
写程序是一般关注值,因此==出现频率较高,而在变量和单例值之间比较时应该使用is。除此之外,is运算符比==快,因为它不能重载,解释器不需要寻找并调用特殊方法,直接比较整数id;a==b是语法糖,等同于a.__eq__(b),继承自object的__eq__方法比较两个对象的id,结果与is一样,而覆盖__eq__方法后结果可能就与is结果不同了。
元组是"可变的"
元组保存对象的引用,如果引用的元素是可变的,即使元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指tuple数据结构的物理内容(即引用)不可变,与引用的对象无关。
t1 = (1, 2, [30, 40]) t2 = (1, 2, [30, 40]) print(t1 == t2) True print(id(t1[-1])) 3031106993224 #标识 t1[-1].append(50) print(t1) (1, 2, [30, 40, 50]) #修改t1[-1]列表 print(id(t1[-1])) 3031106993224 #标识没变 print(t1 == t2) False #值改变了,不相等
浅复制与深复制
默认作浅复制
复制列表最简单的方式是使用内置类型的构造方法,以list为例:
l1 = [3, [40, 50], (6, 7, 8)] l2 = list(l1) print(l2) [3, [40, 50], (6, 7, 8)] print(l2 == l1) True #副本与原副本相等 print(l2 is l1) False #副本与原副本指代不同的对象
当然,可变序列都可以用 [:] 来复制,而无论是构造方法还是 [:] 复制都是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用),如果元素是可变的,就会出现问题。
l1 = [3, [40, 50], (6, 7, 8)] l2 = list(l1) l1.append(99) l1[1].remove(50) print(‘l1:‘, l1) print(‘l2:‘, l2) l2[1] += [22, 33] l2[2] += (9, 10) print(‘l1:‘, l1) print(‘l2:‘, l2) #结果 l1: [3, [40], (6, 7, 8), 99] l2: [3, [40], (6, 7, 8)] #对比l1,由于浅复制,追加99对l2无影响,而对元组l1里面的可变对象[40, 50]执行删除操作却影响到了l1,说明l2和l1绑定同一个列表 l1: [3, [40, 22, 33], (6, 7, 8), 99] l2: [3, [40, 22, 33], (6, 7, 8, 9, 10)] #+=操作就地修改列表,因此l2与l1同时被修改,而+=对于元组这种不可变对象来说,会重新创建一个元组,重新绑定给l2,修改后,l2中的那个元组与l1中的不是同一个 print(id(l1[2])) print(id(l2[2])) print(id(l1[2])) print(id(l2[2])) #替换为打印id,发现l2的元组id最后改变了 2377750817600 2377750817600 2377750817600 2377749721512
浅复制容易,但有时会出现不想要也很意外的结果,就需要深复制。
为任意对象作浅复制和深复制
copy模块提供copy用于浅复制和deepcopy用于深复制(副本不共享内部对象的引用)。
定义一个类bus表示校车,有乘客上车下车方法:
import copy class Bus: def __init__(self, passengers=None): if passengers is None: self.passengers = [] else: self.passengers = list(passengers) def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name) bus1 = Bus([‘Alice‘, ‘Bob‘, ‘David‘]) #原校车 bus2 = copy.copy(bus1) #copy方法复制的校车 bus3 = copy.deepcopy(bus1) #deepcopy方法复制的校车 print(id(bus1), id(bus2), id(bus3)) bus1.drop(‘David‘) #bus1的David下车 print(bus2.passengers) print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)) print(bus3.passengers) #结果 2765338083960 2765338084072 2765338083456 #三个不同的Bus对象 [‘Alice‘, ‘Bob‘] #bus2的David消失了 2765338349448 2765338349448 2765337978376 #bus1,bus2共享同一个列表对象,bus3则有另一个列表 [‘Alice‘, ‘Bob‘, ‘David‘] #bus3没有改变
一般来说,深复制不是一件简单的事情。如果对象有循环引用,那么这个朴素算法会进入无限循环。deepcopy函数会记住已经复制的对象,因此能优雅的处理循环引用:
from copy import deepcopy a = [10, 20] b = [a , 30] a.append(b) print(a) c = deepcopy(a) print(c) [10, 20, [[...], 30]] [10, 20, [[...], 30]]
深复制有时处理得太深,对象可能会引用不该复制的外部资源或单例值,此时可以实现特殊方法__copy__()和__deepcopy__(),控制copy和deepcopy的行为。
函数的参数作为引用
python唯一支持的方式是共享传参。类似于java的引用传参。它是指函数各个形式参数获得实参中各个引用的副本,即函数内部形参是实参的别名。
这样,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象标识(即不能把一个对象替换为另一个对象)
不要使用可变类型作为参数默认值
可选参数可以有默认值,但应该避免使用可变对象作为参数默认值。如果使用可变参数,后果见例子:
定义一辆校车,passenger默认值不用None而用[ ]
class Bus: def __init__(self, passengers=[]): self.passengers = passengers def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name) bus1 = Bus([‘Alice‘, ‘Bob‘]) #1车原两人 print(bus1.passengers) bus1.pick(‘Jane‘) bus1.drop(‘Alice‘) print(bus1.passengers) #上一人下一人 bus2 = Bus() #2车空车 bus2.pick(‘David‘) print(bus2.passengers) #上一人 bus3 = Bus() #3车空车 bus3.pick(‘Mike‘) print(bus2.passengers) #上一人 print(bus2.passengers is bus3.passengers) print(bus1.passengers)
奇怪的现象出现了:
[‘Alice‘, ‘Bob‘] [‘Bob‘, ‘Jane‘] [‘David‘] [‘David‘, ‘Mike‘] True [‘Bob‘, ‘Jane‘]
1车正常行驶,3车出现”幽灵学生“,上二车的David出现在了3车。事实上,可看到bus2和bus3引用的是同一个乘客列表。
实例化Bus时,如果传入乘客可以正常运作,但是不为Bus指定乘客的话,奇怪的事情发生,这是因为self.passengers变成了passengers参数默认值的别名。默认值在定义函数时计算,因而默认值变为了函数对象的属性,如果默认值是可变对象,那么后续函数调用都会受到影响。审查Bus.__init__对象
print(Bus.__init__.__defaults__) #‘David‘, ‘Mike‘成为默认乘客 ([‘David‘, ‘Mike‘],)
所以,如果定义的函数接受可变参数,应该慎重考虑调用方是否期望修改传入的参数。例如校车写成深复制那一节的样子。
del和垃圾回收
del语句删除名称,或者说删除标签。(删除一个物品的标签,而不是删除这个物品)del命令可能导致对象被当作垃圾回收,即满足下列条件之一时:
1.删除的变量保存的是对象的最后一个引用
2.无法得到对象
重新绑定也可能会导致对象的引用数量归零,导致对象销毁。
python采用引用计数算法来进行垃圾回收,每个对象都会统计有多少个引用指向自己,当引用计数器归零时,对象就立即销毁。python2.0采用分代垃圾回收算法,用于处理循环引用。
见下例:
import weakref s1 = {1, 2, 3} s2 = s1 def bye(): print(‘bye‘) ender = weakref.finalize(s1, bye) #注册一个回调函数,在{1,2,3}销毁时使用 print(ender.alive) del s1 print(ender.alive) s2 = ‘helloworld‘ print(ender.alive) #结果发现del s1后,对象仍然存活,而s2重新绑定了对象,于是无法获取对象,导致对象被销毁 True True bye False
弱引用
有引用时对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。
弱引用不会增加对象引用数量,引用的目标对象称为所指对象,因此,弱引用不会妨碍所指对象被当作垃圾回收(任何无引用的时候)。弱引用在缓存中很有用,因为我们不想因为被缓存引用着而始终保存缓存对象。
python提供weakref模块来控制弱引用。
weakref.ref
import weakref import sys set1 = {1, 2} print(sys.getrefcount(set1)) #打印引用计数 wref = weakref.ref(set1) #创建弱引用 print(wref) #打印弱引用 print(sys.getrefcount(wref)) set2 = wref() #!!!弱引用时可调用对象,返回的是被引用的对象,若所指对象不存在则返回None print(set2 is set1) print(sys.getrefcount(set1)) set1 = None set2 = None print(wref)
结果:
2 <weakref at 0x0000024BADFA0408; to ‘set‘ at 0x0000024BADEE99E8> 2 True 3 #调用弱引用返回被引用对象绑定到set2,所以引用显示为3 <weakref at 0x0000024BADFA0408; dead> #弱引用失效
初始引用为2的原因是:当使用某个引用作为参数,传递给getrefcount()
时,参数实际上创建了一个临时的引用。
weakref.WeakValueDictionary
WeakValueDictionary类实现一种可变映射,里面的值是对象的弱引用,被引用对象在程序其他地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary中删除。
import weakref class Cheese: def __init__(self, kind): self.kind = kind def __repr__(self): return ‘Cheese(%r)‘ % self.kind stock = weakref.WeakValueDictionary() catalog = [Cheese(‘A‘), Cheese(‘B‘), Cheese(‘C‘), Cheese(‘D‘), Cheese(‘E‘), Cheese(‘A‘)] for cheese in catalog: stock[cheese.kind] = cheese print(sorted(stock.keys())) del catalog print(sorted(stock.keys())) del cheese print(sorted(stock.keys()))
结果:
[‘A‘, ‘B‘, ‘C‘, ‘D‘, ‘E‘] [‘A‘] []
删除引用后[‘A‘]奶酪还在,是因为临时变量引用了对象,这可能导致该变量存在的时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时会被销毁。示例中是全局变量,需显式删除才会消失。
Weak模块还有proxy,WeakSet,WeakKeyDictionary等
//proxy(obj[,callback])函数来创建代理对象。使用代理对象就如同使用对象本身一样,而不需要像ref那样显示调用
//WeakKeyDictionary的键是弱引用,它的实例可以为应用中其他部分拥有的对象附加元数据,这样就无需为对象添加属性
//WeakSet类保存元素弱引用的集合类,元素没有强引用时,集合会把它删除
以上来自《流畅的python》第8章
以上是关于python对象引用和垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章