浅析python 数据存储原理&深拷贝&浅拷贝&is ==

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014665013/article/details/85787884

1.存储方式

在高级语言中,变量是对内存及其地址的抽象。

对于python而言,python的一切变量都是对象,变量的存储,采用了引用语义的方式,存储的只是一个变量的值所在的内存地址,而不是这个变量的只本身
引用语义:在python中,变量保存的是对象(值)的引用,我们称为引用语义。采用这种方式,变量所需的存储空间大小一致,因为变量只是保存了一个引用。也被称为对象语义和指针语义。
值语义:有些语言采用的不是这种方式,它们把变量的值直接保存在变量的存储区里,这种方式被我们称为值语义,例如C语言,采用这种存储方式,每一个变量在内存中所占的空间就要根据变量实际的大小而定,无法固定下来。
在这里插入图片描述

python中的变量都是采用的引用语义,数据结构可以包含基础数据类型,导致了在python中数据的存储是下图这种情况,每个变量中都存储了这个变量的地址,而不是值本身;对于复杂的数据结构来说,里面的存储的也只只是每个元素的地址而已。
在这里插入图片描述

2.存储过程

基础定义:如果一个数据类型,可以将其他的数据类型作为自己的元素,我就认为这是一种数据结构。数据结构的分类有很多种,但是在Python中常用的只有集合、序列和映射三种结构。对应python中的set、list(tuple、str)、dict;常用的数据类型有int、long、float、bool、str等类型。(其中,str类型比较特别,因为从C语言的角度来说,str其实是一个char的集合,但是这与本文的关联不大,所以我们暂时不谈这个问题)

2.1 数据类型重新初始化对python语义引用的影响

变量的每一次初始化,都开辟了一个新的空间,将新内容的地址赋值给变量。对于下图来说,我们重复的给str1赋值,其实在内存中的变化如下图:

str1 = "hello world"
print(id(str1))
#136746623280

str1="new hello world"
print (id(str1))
#136747975792

在这里插入图片描述

2.2.数据结构内部元素变化重对python语义引用的影响

对于复杂的数据类型来说,改变其内部的值对于变量的影响:

#对应图中步骤(1)
lst1= [1,2,3,4,5,6]
print(id(lst1))
#136748035208

#对应图中步骤(2)
lst1.append('new item')
print(lst1)
print(id(lst1))
#[1, 2, 3, 4, 5, 6, 'new item']
#136748035208

#对应图中步骤(3)
lst1.pop()
print(lst1)
print(id(lst1))
#[1, 2, 3, 4, 5, 6]
#136748035208

#对应图中步骤(3)
lst1[0] = 'change test'
print(lst1)
print(id(lst1))
#['change test', 2, 3, 4, 5, 6]
#136748035208

#对应图中步骤(4)
lst1=[1,2,3,4,5,6]
print(id(lst1))
#136747837832

在这里插入图片描述
当对列表中的元素进行一些增删改的操作的时候,是不会影响到lst1列表本身对于整个列表地址的,只会改变其内部元素的地址引用。可是当我们对于一个列表重新初始化(赋值)的时候,就给lst1这个变量重新赋予了一个地址,覆盖了原本列表的地址,这个时候,lst1列表的内存id就发生了改变。上面这个道理用在所有复杂的数据类型中都是一样的。

3.变量赋值

搞明白了上面的内容,再来探讨变量的赋值,就变得非常简单了。

3.1.简单型数据的赋值(以str为例)

str1 = "hello world"
print(id(str1))
#136746623280

str2=str1
print(id(str2))
#136746623280

str1="new hello world"
print(str1)
print(str2)
#new hello world
#hello world

print (id(str1))
print (id(str2))
#136747975792
#136746623280

在这里插入图片描述
我们刚刚已经知道,str1的再次初始化(赋值)会导致内存地址的改变,从上图的结果我们可以看出修改了str1之后,被赋值的str2从内存地址到值都没有受到影响。
看内存中的变化,起始的赋值操作让str1和str2变量都存储了‘hello world’所在的地址,重新对str1初始化,使str1中存储的地址发生了改变,指向了新建的值,此时str2变量存储的内存地址并未改变,所以不受影响。

3.2.复杂数据结构中的赋值

lst1= [1,2,3,4,5,6]
lst2 = lst1
print(id(lst1))
print(id(lst2))
#136748035208
#136748035208

#对应图中蓝线
lst1.append('new item')
print(id(lst1))
print(id(lst2))
#136748035208
#13674803520

在这里插入图片描述
上图对列表的增加修改操作,没有改变列表的内存地址,lst1和lst2都发生了变化。

对照内存图我们不难看出,在列表中添加新值时,列表中又多存储了一个新元素的地址,而列表本身的地址没有变化,所以lst1和lst2的id均没有改变并且都被添加了一个新的元素。

4.拷贝

提供了两种主要的copy方法,一种是普通的copy,另一种是deepcopy。我们称前者是浅拷贝,后者为深拷贝。

4.1.浅拷贝

首先,我们来了解一下浅拷贝。浅拷贝:不管多么复杂的数据结构,浅拷贝都只会copy一层。下面就让我们看一张图,来了解一下浅浅拷贝的概念。在这里插入图片描述
左图表示的是一个列表sourcelist,sourcelist = [‘str1’,‘str2’,‘str3’,‘str4’,‘str5’,[‘str1’,‘str2’,‘str3’,‘str4’,‘str5’]];

右图在原有的基础上多出了一个浅拷贝的copylist,copylist = [‘str1’,‘str2’,‘str3’,‘str4’,‘str5’,[‘str1’,‘str2’,‘str3’,‘str4’,‘str5’]];

sourcelist和copylist表面上看起来一模一样,但是实际上在内存中已经生成了一个新列表,copy了sourceLst,获得了一个新列表,存储了5个字符串和一个列表所在内存的地址。

我们看下面分别对两个列表进行的操作,红色的框框里面是变量初始化,初始化了上面的两个列表;我们可以分别对这两个列表进行操作,例如插入一个值,我们会发现什么呢?如下所示:
在这里插入图片描述

从上面的代码我们可以看出,对于sourceLst和copyLst列表添加一个元素,这两个列表好像是独立的一样都分别发生了变化,但是当我修改lst的时候,这两个列表都发生了变化,这是为什么呢?我们就来看一张内存中的变化图:
在这里插入图片描述
我们可以知道sourceLst和copyLst列表中都存储了一坨地址,当我们修改了sourceLst1的元素时,相当于用’sourceChange’的地址替换了原来’str1’的地址,所以sourceLst的第一个元素发生了变化。而copyLst还是存储了str1的地址,所以copyLst不会发生改变。

当sourceLst列表发生变化,copyLst中存储的lst内存地址没有改变,所以当lst发生改变的时候,sourceLst和copyLst两个列表就都发生了改变。

这种情况发生在字典套字典、列表套字典、字典套列表,列表套列表,以及各种复杂数据结构的嵌套中,所以当我们的数据类型很复杂的时候,用copy去进行浅拷贝就要非常小心。。。

4.2.深拷贝

刚刚我们了解了浅拷贝的意义,但是在写程序的时候,我们就是希望复杂的数据结构之间完全copy一份并且它们之间又没有一毛钱关系,应该怎么办呢?

我们引入一个深拷贝的概念,深拷贝——即python的copy模块提供的另一个deepcopy方法。深拷贝会完全复制原变量相关的所有数据,在内存中生成一套完全一样的内容,在这个过程中我们对这两个变量中的一个进行任意修改都不会影响其他变量。下面我们就来试验一下。
在这里插入图片描述

看上面的执行结果,这一次我们不管是对直接对列表进行操作还是对列表内嵌套的其他数据结构操作,都不会产生拷贝的列表受影响的情况。看了上面的内容,我们就知道了深拷贝的原理。其实深拷贝就是在内存中重新开辟一块空间,不管数据结构多么复杂,只要遇到可能发生改变的数据类型,就重新开辟一块内存空间把内容复制下来,直到最后一层,不再有复杂的数据类型,就保持其原引用。这样,不管数据结构多么的复杂,数据之间的修改都不会相互影响。
我们再来看看这些变量在内存中的状况:
在这里插入图片描述

5. 比较操作符和同一性运算符

Python中有很多运算符,今天我们就来讲讲is和==两种运算符在应用上的本质区别是什么。

在讲is和==这两种运算符区别之前,首先要知道Python中对象包含的三个基本要素,分别是:id(身份标识)、type(数据类型)和value(值)

is和==都是对对象进行比较判断作用的,但对对象比较判断的内容并不相同。下面来看看具体区别在哪。

==比较操作符is同一性运算符区别

==是python标准操作符中的比较操作符,用来比较判断两个对象的value(值)是否相等,例如下面两个字符串间的比较:

5.1.简单数据结构

a = 'cheesezh'
b = 'cheesezh'
a == b
#True

5.2.复杂数据结构

is也被叫做同一性运算符,这个运算符比较判断的是对象间的唯一身份标识,也就是id是否相同。通过对下面几个list间的比较,你就会明白is同一性运算符的工作原理:

x = y = [4,5,6]
z = [4,5,6]
x == y
#True

x == z
#True

x is y
#True

x is z
#False

print id(x)
#3075326572
print id(y)
#3075326572
print id(z)
#3075328140

什么最后一个是False呢?x、y和z的值是相同的,所以前两个是True没有问题。至于最后一个为什么是False,看看三个对象的id分别是什么就会明白了。
只有数值型和字符串型的情况下,a is b才为True,当a和b是tuple,list,dict或set型时,a is b为False。

这里注意了!!!
大家自己试试看a=257,b=257时它们的id还是否会相等。事实上Python 为了优化速度,使用了小整数对象池,避免为整数频繁申请和销毁内存空间。而Python 对小整数的定义是 [-5, 257),只有数字在-5到256之间它们的id才会相等,超过了这个范围就不行了,同样的道理,字符串对象也有一个类似的缓冲池,超过区间范围内自然不会相等了。
总的来说,只有数值型和字符串型,并且在通用对象池中的情况下,a is b才为True,否则当a和b是int,str,tuple,list,dict或set型时,a is b均为False。

可以参考下面的例子:

a = 'aafgadfa'
b = 'aafgadfa'
print (a==b)
print (a is b)
#True
#True

c = 'a@'
d = 'a@'
print (c==d)
print (c is d)
#True
#False

e = 255
f = 255
print (e==f)
print (e is f)
#True
#True

g = 257
h = 257
print (g == h)
print (g is h)
#True
#False

参考文献

猜你喜欢

转载自blog.csdn.net/u014665013/article/details/85787884