python中可变对象/不可变对象、栈/堆管理
内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
代码区: 存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
静态数据区: 存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
栈区: 存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
堆区: new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。
在 Python 中,数字、字符串等基本类型的对象都是不可变的,其大小是固定的,因此可以直接存储在栈空间中。
栈是一种具有后进先出(Last In First Out)特性的数据结构,栈空间通常用于存储函数调用时的局部变量和函数参数等信息。在函数调用过程中,会按照调用顺序依次进入栈中,而在函数返回时,则会按照相反的顺序依次从栈中弹出。因此,栈空间被广泛应用于函数调用和局部变量存储的原因。当函数被调用时,操作系统会为函数分配一块栈空间,用于存储函数调用时需要使用的局部变量和参数等信息。这些信息通常是在函数调用时创建的,当函数返回时,栈空间也会被自动销毁,这样可以确保内存的高效使用和安全性。
在 Python 中,数字、字符串等基本类型的对象都是不可变的,它们的值在创建后就不会发生变化。因此,Python 可以将它们直接存储在栈空间中,不需要像列表、字典等可变类型一样,动态调整内存大小,从而提高了程序的执行效率。而对于可变类型的对象,Python 通常会将它们存储在堆空间中,这样可以动态调整内存大小,确保程序的灵活性和可靠性。
在 Python 中,列表和字典等复杂类型的对象是动态分配的,其大小不固定。为了方便存储和管理,这些对象通常存储在堆空间中。
当定义一个列表或字典时,Python 会先在栈空间中分配一块空间,用于存储该对象的引用。该引用指向一个在堆空间中分配的内存块,该内存块中存储了实际的列表或字典对象。因为列表和字典等对象的大小不固定,可能需要动态地调整内存大小,所以通常使用动态内存分配方式,即在堆空间中分配一块较大的内存池,然后根据实际需要动态地申请和释放内存块。
需要注意的是,在 Python 中,对象的引用和实际的对象是分开存储的,因此,当多个变量引用同一个对象时(变量就是对象的引用),它们都指向同一个内存块。这也就意味着,当其中一个变量改变了对象的值时,其他引用该对象的变量也会受到影响。
可变对象与不可变对象的操作
a = 1
b = a
print(id(a), id(b)) #2780123588912, a, b两者内存地址相同
a = 1
b = a
print(id(a), id(b)) #2780123588912,两者内存地址相同
a = a + 1 # 等效于a += 1
print(id(a + 1)) # 2065685571952
print(id(a)) # 2065685571920
a = a + 1 和 a += 1,对于数字、字符串等基本类型的不可变对象来说,这种操作相当于另外在堆开辟一个内存空间,生成新对象2,然后将内存地址传给对象a。【个人认为是把a拷贝出来然后再进行操作】
a = [1]
b = a
print(id(a), id(b))
a += [1]
print(id(a))
a = a + [1] 和 a += [1],对于列表和字典等复杂类型的这种可变对象来说,这种操作相当于就在对象a的原内存地址进行操作
函数内堆与栈
a = 3
def test01():
b = 4
test01()
test01()
如上图,每一个对象都是个内存块,栈里面建立对象a、然后在堆里生成对象3,并且将对象3的内存地址给对象a,栈里面对象a保存的是3的内存地址;
函数test01
也是作为一个对象;
当调用函数test01
的时候,会新建一个函数栈帧,栈帧存放我的局部对象,局部对象的值放在堆里,调用完后函数栈帧会释放掉。
栈帧
每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量,从百度中我们可以获取信息如下:
栈帧是一块因函数运行而临时开辟的空间。
每调用一次函数便会创建一个独立栈帧。
栈帧中存放的是函数中的必要信息,如局部变量、函数传参、返回值等。
当函数运行完毕栈帧将会销毁。
def foo1():
a = 1
b = 1
LOAD_CONST
是加载局部作用域(函数)的对象,STORE_FAST
是局部作用域(函数)保存值到对象
创建对象foo1
,接着调用对象foo1
,生成foo1
函数栈帧;
碰到foo1
函数内第一个语句时【a = 1】,会在栈帧内生成对象a
,然后=
右边的1
会被LOAD_CONST
加载为对象放入堆中, 接着STORE_FAST
把1
地址传给栈帧中a
对象中;
foo1
函数内第二个语句时【b = 1】同理。
def foo2():
a, b = 1, 2
UNPACK_SEQUENCE(count)
将 TOS
解包为 count 个单独的值
如上图,foo2
函数会生成a
,b
对象,压入栈中,然后将生成((1, 2))对象,放入堆中;
通过UNPACK_SEQUENCE(count)
序列解包,接着把解包结果按顺序写入a
、b
对象中
import dis # 导入dis库
def bar():
a, b = b, a
如上图,bar
函数会生成a
,b
对象,压入栈中;
交换两个最顶层的堆栈项。
浅拷贝与深拷贝
浅拷贝
import copy
a = [1, 2, [3, [1]]]
b = a.copy()
print(id(a) # 1865060003456
print(id(b)) # 1865060003520
print(a, b) # [1, 2, [3, [1]]] [1, 2, [3, [1]]]
a[0] = 100
a[2][1][0] = 100
print(id(a), id(b)) # 1865060003456 1865060003520
print(a, b) # [100, 2, [3, [100]]] [1, 2, [3, [100]]]
浅拷贝出来的对象,内存地址和拷贝对象是不一样的,第一层内存地址值是独立的,所以a[0] = 100
后,a
、b
第一层数会变;
a = [100, 2, [3, [100]]]
a = [1, 2, [3, [100]]]
但是第二层继续引用拷贝对象的内存地址,a[2][1][0] = 100
会影响a
、b
。(拷贝的不独立不完全,所以认为是浅拷贝)
深拷贝
a = [1, 2, [3, [1]]]
b = copy.deepcopy(a)
print(id(a), id(b)) # 2617664876160 2617664876288,内存地址不同
print(a, b) # [1, 2, [3, [1]]] [1, 2, [3, [1]]]
a[2][1][0] = 100
a[0] = 100
print(id(a), id(b)) # 2352369220608 2352366445888
print(a, b) # [100, 2, [3, [100]]] [1, 2, [3, [1]]]
这里可以看到深拷贝出来的,完全不受影响,是完全独立的
参考文章
https://juejin.cn/post/7142675089694130213
https://blog.csdn.net/weixin_41777118/article/details/130396984
https://blog.csdn.net/qq_34159047/article/details/109229108#:~:text=%E7%AE%80%E8%A8%80%E4%B9%8B%EF%BC%9A%20%E5%AF%B9%E4%BA%8E%E5%8F%AF%E5%8F%98%E5%AF%B9%E8%B1%A1%EF%BC%8C%20a%20%2B%3D%201%20%E7%9B%B4%E6%8E%A5%E5%9C%A8%E5%8E%9F%E5%86%85%E5%AD%98%E5%9C%B0%E5%9D%80%E4%B8%8A%E6%93%8D%E4%BD%9C%20a%20%3D,1%20%E5%92%8C%20a%20%3D%20a%20%2B%201%20%E9%83%BD%E6%98%AF%E5%9C%A8%E6%96%B0%E5%BC%80%E8%BE%9F%E7%9A%84%E7%A9%BA%E9%97%B4%E6%93%8D%E4%BD%9
https://docs.python.org/zh-cn/3.8/library/dis.html#opcode-collectionsC