Python 内存管理 进阶详细篇

谈到 python关于内存管理,就不得不谈到变量对象等相关概念了

变量 : 可变化的名称用于接收对象  变量不是装有对象的“容器”,而是贴在对象上的“标签

变化的原因就是,我可以将值赋值给这个函数名

对象:在python中一切皆对象

举例:

a = sqrt(4)

 a 被称为 变量名(就是变量) sqrt(4) 这就是对象,a = sqrt(4) 是一条赋值语句,我们将sqrt(4)

赋值给了a ,a 指向了sqrt(4) 在内存中的地址,在python要产生变量就必须存在赋值语句。

数据的引用: 当我们在创建对象时,python会在内存中给该对象划分一块地址用存储该对象,存储对象不是存储这个空壳子,而是存在对象中的值。

 1.引用数量:

我刚才说过,变量不能孤立存在,必须和赋值语句共存,赋值是从左往右读,指向是从左往右读,

例如 : a = sqrt(4)

赋值:就是将 sqrt(4) 赋值给 a  ,指向 : a 指向对象sqrt(4)的数值

所谓的引用数量就要谈到,指向 ,当一个数据没有被变量指向时,说明这个数据的引用数量为0,当然我们可以存在同一个元素有多个引用

a = sqrt(4)

b = a

c = a + b

print(c)

对象sqrt(4) 被引用了多少次 

如果存在一个数据的引用数量变为了0,python就会清除这个对象在内存中的位置

二、 可变对象和不可变对象

我们谈论可变指的是这个对象的值可以改变,不可变是指这个对象的值不可变

不可变对象: int float str  tuple 

a = 1

print(id(a))#  id() 是python的内置函数 用于查询对象在内存中的位置 

a = a+1

print(id(a))

打印结果 : 

1982899906864
1982899906896

看起来我好想改变了a 的值,将 a = 1 改为了 a = 2,其实不然只是在内存中重新开辟了一个新的位置来存储a = 2 的值

可变对象: dict set list 

可变对象:字典,集合,列表,都属于可变对象,说其可变,是指其内存中的值可变。

a = [1,2,3,4]
print((a))
a[2] = "hello"
print((a))

打印结果:

[1, 2, 3, 4]
[1, 2, 'hello', 4]
 #很明显我修改了变量a中的值

从底层看:程序中的数据都要放在内存条内,变量就是这块内存的名字。

"""
1.深拷贝和浅拷贝
浅拷贝
如果被拷贝的对象是不可变对象,则不会生成新的对象
如果被拷贝对象是可变对象,则会生成新的对象,但是只会对可变对象最外层进行拷贝
深拷贝
如果被拷贝对象是不可变对象,深拷贝不会生成新对象,因为被拷贝对象是不可变的,继续用原来的那个,不会产生什么坏的影响
如果被拷贝对象是可变对象,那么会彻底的创建出一个和被拷贝对象一模一样的新对像
一旦出现新对象说明这个变量所指的数据和原数据共用一块内存
说可变,是指这个内存对象的值可以改变,说不可变,是指这个内存对象的值不可以改变
以字符串举例 字符串不支持项分配,想当然你也不能对它重新赋值更不可以改变它
str_ = 'String is an immutable object'
# str_[4] = "kk"
print(str_)
print(str_[4])
TypeError: 'str' object does not support item assignment
int_ = 55
int_ = 22
print(int_)
如果被拷贝对象是可变对象,则会生成新的对象,但是只会对可变对象最外层进行拷贝
举例:
list  是可变对象
import copy

list_ = [1,2,3,4,'str_','float_']
print(id(list))
print(id(list_[4]),id(list_[5]))
list_copy = copy.copy(list_)
print(id(list_copy))
print(id(list_copy[4]),id(list_copy[5]))
#OutputResults
140716703656688
2638873443760 2638874710064
2638877800384
2638873443760 2638874710064
From the generated results, we can conclude whether the element positions in a new shell were only generated from the original data or not
因为它们的数据元素来源于同一片内存位置,当然内存地址是相同的,因此在对新拷贝的对象做修改一定会影响到原来数据因此要谨慎行事


# 在深拷贝中,被拷贝的对象是不可变对象,深拷贝则不会生成新的对象,也就是不会在内存中为它开配一块新的位置,一切的一切还是使用原来的存储位置
# 如果被拷贝的对象是可变对象,那么会彻底创建一个新的对象出来,主要讲的还是对象的存储位置,对象中的元素的存储位置
#举例一个列表嵌套
import copy
list_ = [[1],2,'str',(tuple)]
print(id(list_[1]))
print(id(list_))
Depth_copy = copy.deepcopy(list_)
print(id(Depth_copy[1]))
print(id(Depth_copy))
#Output_Ruslet
2946321377616
2946326181952
2946321377616
2946323360384
# 为什么会出现这样一个结果因为虽然对象整体是一个list是可变对象,但是list中存在int int 是不可变对象所以int的内存地址并没有发生变化也就是没有生成新的对象
#在深拷贝下对被拷贝对象做修改不会影响到原来的对象
关于python垃圾回收机制:

分代回收
标记清楚
引用计数(对象引用计数)
引用计数的优点:
简单 实时性高,只要引用计数为0,对象就会被销毁,内存被释放,回收内存的时间平摊到了平时
当一个对象的引用计数为0时就会进入销毁阶段,这个对象在内存中就会被删除
2. 标记清除
引用计数,并不能解决所有的问题,一旦出现了循环引用,那么,这些对象的引用次数永远都是大于0的,但是这些对象都是不可用的垃圾数据。下面的代码展示了一种循环引用的情况
所谓的循环引用,类似函数调用自己
a = 1
b = 2
b = a + b
a = a + b     (a = a + a + b)
b = a + b     (a + a + b + a + b)
所以说一旦出现了循环引用,那么这些对象的引用次数永远都是大于0或者大于自己上一个引用次数
但是不乏出现很多垃圾数据或者无效数据
标记清除可以处理这种循环引用的情况,它分为两个阶段
第1阶段,标记阶段
GC会把所有活动对象打上标记,这些活动的对象就如同一个点,他们之间的引用关系构成边,最终点个边构成了一个有向图,如下图所示
第2阶段,搜索清除阶段
从根对象(root)出发,沿着有向边遍历整个图,不可达的对象就是需要清理的垃圾对象。这个根对象就是全局对象,调用栈,寄存器。
在上图中,从root出发后,可以到达 1 2 3 4,而5, 6, 7均不能到达,其中6和7互相引用,这3个对象都会被回收。



#分代回收:
分代回收建立标记清楚的基础之上,是一种以空间换时间的操作方式,标记清楚可以回收循环引用的垃圾,但是回收的频次是需要控制的
分代回收,根据内存中对象的存活时间将他们分为3代,新生的对象放入到0代,如果一个对象能在第0代的垃圾回收过程中存活下来,GC就会将其放入到1代中,如果1代里的对象在第1代的垃圾回收过程中存活下来,则会进入到2代。

"""
import gc
print(gc.get_threshold())
#打印结果:   (700, 10, 10)
当分配对象的个数减去释放对象的个数的差值大于700时,就会产生一次0代回收
第一个10代表1代回收 ,第二个10代表2代回收,如果在0代回收中的数据经过10次回收,没有被清除掉,就会进入第二个10次,10次1代回收没有被清除掉,就会来到第二代
从第0代到第1代第2代,每增加一代,被检测机会就会降低
当然你可以可以设置你python程序引用次数的回收机制
例如:
import gc

gc.set_threshold(600, 10, 5)
print(gc.get_threshold())
# 当分配对象的个数减去释放对象的个数差值大于600时就会执行一次0代回收,经历10次0代回收会经历一次一代回收,经历5次一代回收,就会执行一次二代回收
600  关于进入到第0代  10   关于进入到第一代  5 关于进入到第二代

转载cool python 链接:深度讲解python垃圾回收机制 | 酷python (coolpython.net)

猜你喜欢

转载自blog.csdn.net/weixin_59131972/article/details/130267002