Python内存分配及深浅拷贝

写在最前:网上关于python深浅拷贝的内容很多,写的也比较好。而我写这篇主要是想强迫自己总结一下这个主题,也想让自己的理解更深刻一些。

1.内存分配机制

首先,程序在内存和CPU中保存并执行时,系统会将程序中的各项数据进行分解,将不同的数据加载到内存的不同区域。通常,内存中的区域可以分为一下4个部分:

内存区域作用
栈内存区(Stack)用于直接分配数据,存取数度较快,数据存储不稳定,适用于小数据块的快速存取,一般在程序中用于存储对象的引用。每个线程都会有一个私有的栈。
堆内存区(Heap)存储数据稳定持久,一般用于存储加载较为重量级的数据,如程序运行过程中的对象都是存在堆内存中的。
静态区(Static)主要用于加载存储程序中的一些静态数据、常量数据等等,python中的不可变数据类型的数据也会存储在静态常量区内存中。
方法区 (Data)主要用于加载程序中的代码数据、二进制数据、方法数据等等程序运行需要的预加载数据。

Python和Java一样都是“万物皆对象”(如下图)。所以按照上述的数据保存原则,则应当是a,b,c,d,e都保存在栈区,因为他们都是对象的引用。‘1’和1是不可变类型,则应该保存在静态区,剩下的[1]和set(……)(使用set定义的集合是可变的,使用frozenset定义的则是不可变类型的集合)则应该保存在堆区。

>>> a=1
>>> a.__class__
<class 'int'>
>>> b=[1]
>>> b.__class__
<class 'list'>
>>> c=(1,)
>>> c.__class__
<class 'tuple'>
>>> d='1'
>>> d.__class__
<class 'str'>
>>> e=set([1,3,34,2])
>>> e.__class__
<class 'set'>

2.python的可变对象和不可变对象

关于python的深浅copy其实跟python中的可变对象和不可变对象有关,所以先来说说这两种对象类型。

可变对象:常见的可变对象有list、dict、set。而不可变对象有:数字,字符串、tuple。

可变对象和不可变对象的主要区别则在于:可变对象可以“原址修改”,也就是说可变对象所指向的内存地址的值是可以发生变化的;而不可变对象所指向的内存地址所保存的值是不能改变的,要改变不可变对象所指向的值,只能重新开辟一块内容空间给它。

>>> a='a'
>>> id(a)
1871343910560
>>> a='b'
>>> id(a)
1871343908880
>>> b=[1]
>>> id(b)
1871353823688
>>> b.append(2)
>>> b
[1, 2]
>>> id(b)
1871353823688

Python为了快速存储字符串及整型,对一些常用到的整型和较短的字符串进行了缓存。对于Python3,其整型数据的缓存范围为[-5,256]。而对于浮点型数据,虽然同样为不可变类型的数据,但是python并不对其进行缓存。

>>> a=1
>>> b=1
>>> id(a)==id(b)
True
>>> a='a'
>>> b='a'
>>> id(a)==id(b)
True
>>> a=300
>>> b=300
>>> id(a)==id(b)
False
>>> a=1.0
>>> b=1.0
>>> id(a)==id(b)
False

从这面的实验结果看出,对于由python进行了缓存的数据,其保存地址是固定的,所以id(a)==id(b)的结果为True.其内部的数据保存结构大致如下。

而对于未被Python缓存的数据,需要注意以下这种情况。

>>> a=300
>>> b=300
>>> id(a)==id(b)
False
>>> a,b=300,300
>>> id(a)==id(b)
True

对于a,b单独分行赋值的情况,这个结果与我当初设想的不同。因为整型数据缓存的存在,我一直以为python中所有的整型数据在一次程序运行期间,只会有一个保存地址,所有指向这个值的变量的引用都是相同的。这是对不可变类型的误解。出现这种情况反而加深了对可变对象和不可变对象的理解。而于第二种情况,这个从网上搜索出来的原因说是cpython导致的。

从下面的代码段中可以得出一个结论:按照不可变类型的内存分布区域来看,保存在静态区的数据是不能在本地进行改变的,但是对于非缓存的不可变类型,在静态区的保存地址不是唯一固定的(说法可能不准确)。

>>> a=300
>>> b=300
>>> id(a)==id(b)
False
>>> a,b=300,300
>>> id(a)==id(b)
True

3.深浅拷贝

先说一下深浅拷贝的定义。浅拷贝只是拷贝了一个对象的引用,所有指向这个对象的变量共用同一段内存地址,其中一个变量的值发生改变,另一个变量的值也会发生改变。而深拷贝是一个变量对另一个变量的值的拷贝,不同的变量占用不同的内存地址,并且变量之间的改变互不影响。

3.1 不可变对象的深浅拷贝

不可变对象是没有深拷贝的,即使是调用深拷贝函数进行拷贝。

>>> from copy import deepcopy
>>> a='a'
>>> id(a)
1805273857696
>>> b=deepcopy('a')
>>> b
'a'
>>> id(b)
1805273857696

对于不可变对象进行浅拷贝的时候,如果原引用值发生了改变,指向了其他对象。但是拷贝出的对象依然可以指向原来的对象。程序如下。这就是因为不可变对象不能原地修改导致的。a的值从100变成字符串'1'的过程,不是将原本保存100的内存的值变成'1',而是将a保存的内存地址改成'1'所在的内存地址,而b对应的内存地址并未发生改变,所以b的值仍为100。

>>> a=100
>>> id(a)
1457158256
>>> b=a
>>> id(b)
1457158256
>>> a='1'
>>> id(a)
1342944983168
>>> b
100
>>> id(b)
1457158256

2.2 可变对象的深浅拷贝

>>> a=[1,2,[1,2]]
>>> id(a)
2252725804552
>>> b=a
>>> b
[1, 2, [1, 2]]
>>> id(b)
2252725804552
>>> b[0]=3
>>> b
[3, 2, [1, 2]]
>>> a
[3, 2, [1, 2]]
>>> b[2][1]='a'
>>> b
[3, 2, [1, 'a']]
>>> a
[3, 2, [1, 'a']]

从上面的实验结果可以看出,赋值语句的作用是浅拷贝。变量b和变量a指向同一片栈内存地址。又由于不可变对象是可以本地修改的,所以当b的list的值进行修改时,a也同样发生了变化。下面来看看复制。

代码一:

>>> from copy import copy
>>> a=[1,2,[1,2]]
>>> id(a)
1740736217032
>>> b=copy(a)
>>> id(b)
1740736216328
>>> b[0]='a'
>>> b
['a', 2, [1, 2]]
>>> a
[1, 2, [1, 2]]

代码二:

>>> from copy import deepcopy
>>> a=[1,2,[1,2]]
>>> id(a)
2102071971656
>>> b=deepcopy(a)
>>> b
[1, 2, [1, 2]]
>>> id(b)
2102071970952
>>> b[0]='a'
>>> b
['a', 2, [1, 2]]
>>> a
[1, 2, [1, 2]]

从上面的实验结果可以发现,似乎copy和deepcopy都实现了我们想要的功能。下面来仔细看看copy和deepcopy的区别。先看下面一段代码的执行结果。

>>> from copy import copy,deepcopy
>>> a=[1,2,[1,2]]
>>> b=copy(a)
>>> c=deepcopy(a)
>>> a
[1, 2, [1, 2]]
>>> id(a)
1960234196616
>>> b
[1, 2, [1, 2]]
>>> id(b)
1960234246024
>>> c
[1, 2, [1, 2]]
>>> id(c)
1960234246088
>>> a[0]='a'
>>> a
['a', 2, [1, 2]]
>>> b
[1, 2, [1, 2]]
>>> c
[1, 2, [1, 2]]
>>> a[2][0]='b'
>>> a
['a', 2, ['b', 2]]
>>> b
[1, 2, ['b', 2]]
>>> c
[1, 2, [1, 2]]
>>> a.append(4)
>>> a
['a', 2, ['b', 2], 4]
>>> b
[1, 2, ['b', 2]]
>>> c
[1, 2, [1, 2]]
>>> a[2].append(5)
>>> a
['a', 2, ['b', 2, 5], 4]
>>> b
[1, 2, ['b', 2, 5]]
>>> c
[1, 2, [1, 2]]

从上面的实验结果可以看出copy和deepcopy的区别,在对a[0]进行改变时,b和c都不受a的变化的影响,但当对a[2]中的元素进行修改时,b发生了跟a一样的变化,但c仍然不受影响。显然,这两个函数对数复合对象的处理有所不同。对于copy而言,它复制了对象,但对于对象中的元素,依然使用引用,而对于deepcopy,则通过创建新的复合对象并重复复制x的所有成员来创建x的深复制。所以真正的深拷贝,只能使用deepcopy

>>> from copy import copy,deepcopy
>>> a=[301,302,[1,2]]
>>> b=copy(a)
>>> id(a[0])
2420078469328
>>> id(a[2])
2420078510280
>>> id(b[0])
2420078469328
>>> id(b[2])
2420078510280
>>> c=deepcopy(a)
>>> id(c[0])
2420078469328
>>> id(c[2])
2420078403528

参考文献:

https://www.jianshu.com/p/2f98dd213f04

https://www.cnblogs.com/mcznhaha/p/4820068.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值