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对象引用和垃圾回收的更多相关文章
- python内存管理&垃圾回收
python内存管理&垃圾回收 引用计数器 环装双向列表refchain 在python程序中创建的任何对象都会放在refchain连表中 name = '张三' age = 18 hobby ...
- Python中的垃圾回收与del语句
python中的垃圾回收采用计数算法 一个对象如果被引用N次,则需要N次(即计算引用次数为零时)执行del 才能回收此对象. a = 100 b = a del a print(b) print(a) ...
- Python中的垃圾回收机制
Python的垃圾回收机制 引子: 我们定义变量会申请内存空间来存放变量的值,而内存的容量是有限的,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉,而变量名是访问到变量值的唯一方式,所以 ...
- python del和垃圾回收
1. del是删除对象 2. python中的垃圾回收是删除引用计数
- Python 中的垃圾回收机制(转载)
from: https://foofish.net/python-gc.html GC作为现代编程语言的自动内存管理机制,专注于两件事:1. 找到内存中无用的垃圾资源 2. 清除这些垃圾并把内存让出来 ...
- python进阶之垃圾回收
内存使用: 程序在运行的过程需要开辟内存空间,比如创建一个对象需要一片内存空间,定义变量需要内存空间.有内存的开辟,势必也要有内存的释放,不然只进不出那不是貔貅了吗?即使有开辟有释放在短期内还是会有垃 ...
- python中的垃圾回收机制及原理
序言: 来一起看看: 不同于C/C++,像Python这样的语言是不需要程序员写代码来管理内存的,它的GC(Garbage Collection)机制 实现了自动内存管理.GC做的事情就是解放程序员的 ...
- python里面的垃圾回收机制
文章链接:https://www.jianshu.com/p/1e375fb40506 Garbage collection(GC) 现在的高级语言如java,c#等,都采用了垃圾收集机制,而不再是c ...
- 【转载】Python中的垃圾回收机制
GC作为现代编程语言的自动内存管理机制,专注于两件事:1. 找到内存中无用的垃圾资源 2. 清除这些垃圾并把内存让出来给其他对象使用.GC彻底把程序员从资源管理的重担中解放出来,让他们有更多的时间放在 ...
随机推荐
- 题解 e
传送门 第一眼看貌似可以树剖,然而那个绝对值不知怎么维护 求最小连通块我只会\(k^2\) 主席树貌似可以用来查询区间内与某个数差的绝对值的最小值? 确实,每次查大于等于该数的最小数和小于等于该数的最 ...
- ESP32CAM 人脸识别追踪
引言 总体实现的流程:ESP32cam作为客户端,pc作为服务端通过mqtt协议建立通信,将采集的图像在电脑端显示人脸识别的方法使用的是opencv,并通过mqtt传输指令给esp32cam控制舵机云 ...
- uwp Button的动态效果
你应该覆盖Button样式 <Page.Resources> <Style TargetType="Button" x:Key="CustomButto ...
- 【转】Java 开发必会的 Linux 命令
转自:https://www.cnblogs.com/zhuawang/p/5212809.html 作为一个Java开发人员,有些常用的Linux命令必须掌握.即时平时开发过程中不使用Linux(U ...
- Java之Apache Commons-IO使用精讲
Commons IO是针对开发IO流功能的工具类库.主要包括六个区域: 工具类--使用静态方法执行共同任务输入--用于InputStream和Reader实现输出--用于OutputStream和Wr ...
- ArcGIS:从DEM数据提取对应点的高程
通过Extract Value to Points从DEM数据中提取所需点的高程. 方法/步骤 将DEM数据文件和一个shapefile点文件(分别命名为"DEM"和"P ...
- Thread类的常用方法----多线程基础练习
创建多线程程序的第一种方式----继承Thread类 常用API 构造方法 public Thread() :分配一个新的线程对象. public Thread(String name) :分配一个指 ...
- servlet中servletContext的五大作用(五)
1. 获取web的上下文路径 2. 获取全局的参数 3. 作为域对象使用 4. 请求转发 5. 读取web项目的资源文件 package day10.about_serv ...
- github push报LibreSSL SSL_connect错误
最近发现在家里push代码到github的时候总是报错,报错内容如下: fatal: unable to access 'https://github.com/MangoDowner/clear-le ...
- Go版本依赖--伪版本
目录 1.简介 2. 什么是伪版本 3. 伪版本风格 4. 如何获取伪版本 1.简介 在go.mod中通常使用语义化版本来标记依赖,比如v1.2.3.v0.1.5等.因为go.mod文件通常是go命令 ...