深入理解Python变量的数据类型和存储
作者:zz大星星
我们知道,python的变量是有类型的,对于python变量的几种数据类型,我们在写python时是必须要有一定的概念的。知道数据类型就要知道变量数据类型怎么存储,可是为什么python的变量不需要声明数据类型就可以直接赋值?变量如果有数据类型,那变量不是可以为任意数据类型?那真正的数据类型如int在内存存储的字节大小应该为多少?等等诸如一系列的问题让我提起了的兴趣,经过网上不断查找学习后,在此将我所了解到的内容在此做个总结归纳
一、变量的数据类型
1、什么是变量的数据类型
我们先捋一捋什么是变量,变量从字面上理解就是可以变化的量,我们可以随时改变这个变量的值,使得我们可以调用同一个变量而获得不同的值,与之对应的是常量。那么对于一个可变的变量,它有可能表示是一个字符串,一个数字或者是一个小数,因为这些在计算机内存里存放的方式是不一样的,所以简单理解就是变量的数据类型不同就是对应的数据在计算机内存中存放方式的不同。这种方式表现在按多少字节存储,是否连续存储等。
我们都知道,c是静态类型语言,一种在编译期间就确定数据类型的语言,也就是我们需要对变量先声明其数据类型后才能使用,并且在使用过程中一般不能赋值一些超过该数据类型数值,比如:int a = 1.2,当然大类型是可以转向小类型的,如:double a = 1 (double类型接收整形数值)。可以肯定的,大多数静态类型语言都这么干。
当然,python语言也有数据类型。但python语言不同,它是一种动态类型语言,又是强类型语言。它们确定一个变量的数据类型是在你第一次给它赋值的时候,也就是说你赋值给变量什么数据类型的数值,变量就是什么数据类型的。所以,对比之下,c语言变量的数据类型是事先定义的,而python是后天接受的。
2、python五大标准数据类型
在讲变量存储之前,这里先简单总结下python的五大标准数据类型,为了方便展示,我们采用type方法显示变量的数据类型。
(1)Numbers(数字)
数字数据类型用于存储数值。他们是不可改变的数据类型,可简单分为以下四种:(注意这里十六进制,八进制都属于int整形。)
int(整型):
var = 520 print(type(var)) # <class 'int'>
float(浮点型):
var = 5.20 print(type(var)) # 输出:<class 'float'>
bool(布尔型):
var = true print(type(var)) # 输出:<class 'bool'>
complex(复数):
var = complex(13,14) print(type(var)) # 输出:<class 'complex'>
(2)String(字符串)
字符串或串是由数字、字母、下划线组成的一串字符,用‘',“”,“‘ '”都可表示。三者的使用可参考这篇文章: python字符串的各种表达方式.
如下方代码所示,获得的类型为str类型。另外也顺便提一个小知识点,要访问字符串可以正向访问也可以反向访问,即正向时,var[0] = ‘p',var[1] = ‘i',var[2] = ‘g';而反向时,var[-1] = ‘g',var[-2] = ‘i',var[-3] = ‘p'。
var = “pig” print(type(var)) # 输出:<class 'str'> print(var[0:3]) # 正向访问,输出:'pig' print(var[-1]) # 反向访问,输出:'g'
(3)List(列表)
列表是 Python 中使用最频繁的数据类型,用 [ ] 标识。列表可以完成大多数集合类的数据结构实现。它可以同时包含字符,数字,字符串甚至可以包含列表(即嵌套)。如下方代码所示,列表的处理方式和字符串类似。
var = [ 'pig' , 1 , 2.2 ] print(type(var)) # 输出:<class 'list'> print(var[0]) # 获得第一个元素,输出:'pig' print(var+var) # 打印组合的列表,输出:[ 'pig', 1 , 2.2,'pig', 1 , 2.2 ]
(4)Tuple(元组)
元组类似于 List(列表)。元组用 () 标识。内部元素用逗号隔开。但是元组不能二次赋值,相当于只读列表。
var = ( 'pig', 1 , 2.2 ) print(type(var)) # 输出:<class 'tuple'> print(var[0]) # 获得第一个元素,输出:'pig' print(var+var) # 打印组合的元组,输出:( 'pig', 1 , 2.2,'pig', 1 , 2.2 ) var[0] = 'dog' # 出错!不能被二次赋值
(5)Dictionary(字典)
字典的相对于列表来说,列表是有序的对象集合,而字典是无序的对象集合。两者之间的区别在于字典当中的元素是通过键来存取的,而不是通过偏移存取。字典用"{ }"标识,字典由索引key和它对应的值value组成。
dic = {'name':'张三','age':18} print(dic ['name']) # 得到键为'name' 的值,输出:'张三' print(dic [age]) # 得到键为'age' 的值,输出:18 print(dic) # 得到完整的字典,输出:{'name':'张三','age':18} print(dic.keys()) # 得到所有键,输出:dict_keys:(['name','age']) print(dic.values()) # 输出所有值,输出:dict_values:(['张三',18])
二、python变量的存储
1、变量与储存地址的关系
在高级语言中,变量是对内存及其地址的抽象。以c语言举例, 变量事先定义好一种数据类型,于是编译器为变量分配一个对应类型的地址空间和大小(如int 4字节,char 1字节),当该变量改变值时,改变的只是这块地址空间中保存的值,即在程序运行中,变量的地址就不能再发生改变了。这种存储方式称为值语义。如下代码用VS2015运行,由结果可知,test变量的值被存储在0x0020FDC8,当变量改变时,地址不变,地址中对应的值发生改变。
#include<iostream> using namespace std; int main() { int test = 1; cout << &test << ":" << test << endl; test = 2; cout << &test << ":" << test << endl; return 0; }
运行结果:
0020FDC8:1
0020FDC8:2
这里就存在一个问题,每次新建一个变量,编译器就会开辟一块对应数据类型大小的内存,然后给那块内存取个名字(变量名)。除非一块内存被释放,那么该变量才能释放,不然一个变量就只能固定地对应一个数据类型。
对此,python做出了改变,它采用了与高级语言截然不同的方式。在python中,一切变量都是对象,变量的存储采用了引用语义的方式,存储的只是一个变量的值所在的内存地址,而不是这个变量的值本身。简单理解就是,python变量只是某个数据的引用(可以理解成C语言的指针),当python变量赋值时,解释器(因为python为解释性语言)先为数值开辟一块空间,而变量则指向这块空间,当变量改变值时,改变的并不是这块空间中保存的值,而是改变了变量的指向,使变量指向另一个地址空间。这种存储方式称为对象语义或指针语义。举个例子:
str = 'girls are pig' print(id(str)) str = 'boys are dog' print(id(str))
运行结果:
113811696
113812464
id()方法可以获得变量指向的地址,由运行结果所示,一开始变量指向了113811696这个地址,这个地址存放了‘girls are pig'这个字符串,当变量发生改变时,即该变量的指向改变了,指向地址113812464,该地址存放有‘boys are dog'这个字符串。这两个字符串都是一开始解释器先在内存开辟好的。
所以,这也就解释了为什么python的变量被整形赋值就成了整形,被列表赋值就成了列表,变量可以为任意数据类型的,因为python的变量只是对编译器事先在内存存放好的数据的引用。
python采用这种方式,好处就体现在,对于解释器来说,变量就只是一个地址的引用,而这个引用是可以随时改变的,那么就可以做到一个变量用来指向各种各样的数据类型,只要每次记录变量与哪个数据类型连接就行了,效率不就提升了嘛~。而对于c语言的编译器来说,一个变量就只能与一个数据类型长相厮守,所以它望着记录了各种各样变量名与内存值对应的表格,一边编译,一边陷入了沉思…(这里注意一点牛角尖,变量名只是给解释器看的东西,在内存是不做存储的,真正存储的是变量名对应的内容,上面说的变量都是int a中a这个个体)
2、复杂数据类型的存储方式
这里说的复杂数据类型主要是像列表,字典等这种可以改变内部数据的数据类型。以列表作为例子举例,代码如下所示:
list1 = [1,2,3] print(list1) #输出:[1,2,3] print(id(list1)) #输出:112607104(不同电脑分配给变量的地址不同) list1[0] = "hello" print(list1) #输出:['hello',2,3] print(id(list1)) #输出:112607104 list1.append(4) print(list1) #输出:['hello',2,3,4] print(id(list1)) #输出:112607104 list1 = ['hello',4] print(list1) #输出:['hello',4] print(id(list1)) #输出:112925120
由运行结果所示,无论对列表list1进行什么增删改查操作,都不会影响list1本身的存储,只是改变了存储的内容,但list1重新赋值时,地址则发生改变。这个为了更好地解释清楚一点,就拿出我自豪的画画天赋吧(手动狗头,咳咳),上图~。
先声明一点,一个变量存有某一个对象的地址即等于该变量指向了这个对象。上面解释了,list1变量存放的是某个数据类型的引用,换种说法就是存放某个对象的地址,这里就是存放一个列表的地址,即list1变量指向了列表。如图所示,第一步,list1变量指向列表1,该列表存放着三个可变元素list1[0],list1[1],list1[2],它们分别存放着不同对象(值)的地址。第二步,列表的第一个元素list1[0]发生改变,变成存放hello这个字符串对象的地址。第三步,列表新增了一个元素,该元素存放了新的整形对象4的地址。第四步,列表变量list1重新赋值,指向了新的列表2,列表2元素又指向了hello和4这两个对象。
因此,前面三步,因为都是改变了列表元素的指向,变量本身的指向没有变化,即变量的地址也没有变化,但第四步,变量进行重新的赋值,即指向了新的列表,那么变量的地址变发生了变化。
这里也有重要的一点是,列表2和列表1指向的对象hello和4是一致的,因为它们的对象是一样的,所以它们共用一个对象。从下面代码可以体现,输出的结果是一致的。
list1 = ['hello',2,3,4] print(id(list1[0])) #输出:112926064 print(id(list1[3])) #输出:8791404644096 list2 = ['hello',4] print(id(list2[0])) #输出:112926064 print(id(list2[1])) #输出:8791404644096
3、变量的赋值——浅拷贝和深拷贝
(1)变量赋值的安全隐患
因为python的这种变量是一个对象的引用的机制,必然导致的结果是两个变量赋值时会产生相互牵连的现象。举个例子,list1赋值为[1,2,3],然后将其赋值给list2,改变list1时,我们可以发现list2也发生改变。代码如下。
list1 = [1,2,3] list2 = list1 print(list1) #输出:[1,2,3] print(list2) #输出:[1,2,3] print(id(list1)) #输出:112607104 print(id(list2)) #输出:112607104 list1[0] = 'hello' print(list1) #输出:['hello',2,3] print(list2) #输出:['hello',2,3] print(id(list1)) #输出:112607104 print(id(list2)) #输出:112607104
解释图如下,第一步,list1变量指向了列表1,经过赋值后,变量list2也指向了列表1,因此两者地址相同。第二步,变量list1改变第一个列表元素的值,使其指向‘hello',这时我们访问list2内容时,因为list1和list2指向的列表一致,所以list2就变成改变后的值。
由此引出主题深拷贝和浅拷贝,所谓深拷贝呢,就是一个变量的内容赋值给另一个变量时,是把全部资源重新复制一份再赋值给新的变量,而浅拷贝则不然,它的赋值只是将资源的地址给新的变量,二者同时共享该资源。显然,上面的赋值运算例子就是一个浅拷贝。
(2)浅拷贝
为了更加深入了解二者,举一个稍微复杂一丢丢的例子,这里我们需要用到外部包,copy包,它的方法copy()就是一个浅拷贝,而deepcopy()就是一个深拷贝。先举例浅拷贝,这次采用嵌套列表并且使用copy方法来进行拷贝。对比输出结果可以看到对列表list1和list2进行操作时,两者没影响,但对peope这个列表操作时,则两个列表都有影响。
import copy people = ['girl','boy'] list1 = [1,2,people] list2 = copy.copy(list1) print(list1) #输出:[1,2,['girl','boy']] print(list2) #输出:[1,2,['girl','boy']] list1.append('hello') list2.append('hi') print(list1) #输出:[1,2,['girl','boy'],'hello'] print(list2) #输出:[1,2,['girl','boy'],'hi'] people[0] = 'pig' print(list1) #输出:[1,2,['pig','boy'],'hello'] print(list2) #输出:[1,2,['pig','boy'],'hi']
由下图可知,第一步,list1和list2分别指向列表1和列表2,其元素也指向对应的值,但个列表的第三个元素都指向了同个列表。第二步,list1产生新元素,指向‘hello',list2产生新的元素,指向‘hi'。第三步,people这个列表的第一元素地址指向从‘girl'变成了‘pig'(狗头保命),因为是共用列表,所以list1和list2这两个变量都产生了变化。从中也可以分析得到,copy这个方法不像‘='这种赋值运算,它拷贝了资源的第一层,但如果有该资源有第二层时,则变成共用资源,这也是比较容易被忽略的一点。
(3)深拷贝
为了解决浅拷贝带来的安全隐患,有时我们需要采用深拷贝来拷贝我们的资源。即python的copy模块提供的另一个deepcopy方法。深拷贝会完全复制原变量相关的所有数据,在内存中生成一堆一模一样的资源,在这个过程中我们对这两个变量中的一个进行任意修改都不会影响其他变量。我们来测试一下。
import copy people = ['girl','boy'] list1 = [1,2,people] list2 = copy.deepcopy(list1) print(list1) #输出:[1,2,['girl','boy']] print(list2) #输出:[1,2,['girl','boy']] list1.append('hello') list2.append('hi') print(list1) #输出:[1,2,['girl','boy'],'hello'] print(list2) #输出:[1,2,['girl','boy'],'hi'] people[0] = 'pig' print(list1) #输出:[1,2,['pig','boy'],'hello'] print(list2) #输出:[1,2,['girl','boy'],'hi']
流程如下图所示,其步骤和浅拷贝的步骤一致,但不同的一点是,步骤三的people列表改变时,只有list1变量的people列表中‘girl'变成‘pig',而list2变量没什么影响,二者完全独立。
三、python变量数据类型的大小
本来探索到上面已经差不多要结束,鬼知道我脑子又冒出了个奇怪的想法,python的int类型到底要占有电脑的多少个字节呢。毕竟习惯了c语言,而python对变量神奇的设计总是散发着它独特的魅力。所以找啊找,找到一个可以显示数据大小的API函数getsizeof(),只要导入sys包即可。那么写个例子:
import sys print(sys.getsizeof(0)) # 输出:24 print(sys.getsizeof(1)) # 输出:28 print(sys.getsizeof(2)) # 输出:28 print(sys.getsizeof(2**15)) # 输出:28 print(sys.getsizeof(2**30)) # 输出:32 print(sys.getsizeof(2**128)) # 输出:44
看到输出结果,属实让人震惊,一个int型的数值,居然用高达24个字节来存储,而且在电脑存储大小居然是不限定的,是自增长的。喝口水压压惊后,让我想到c++的STL容器,可以使用栈顶指针,当检测到容量超出时,则删除旧内存而去开辟一块新的内存,确实可以实现这种效果。
扯完犊子,那么这里首先先解决第一个问题,int类型这个变量什么时候内存会变大?我在这篇博客中提到的文章找到了答案: 点此处跳转。重点就是下面这张图,简单来说就是int类型每多2^30(1073741824 )就会增加四个字节。这也验证了上面例子getsizeof(2**30)是32字节,而比它小的是28个字节的原因,当然零除外。其他类型也可以在下面找到答案。
那么它的自增长问题呢,这个可能要去看python的源码才能解决,还好有大佬已经提前给我们铺了下路,这里我就没这个能力去了解太深入了,直接引用大佬的结论就可以了。具体可以参考这篇文章:点此处跳转。在64位python的解释器中,int类型的定义是通过一个结构体来定义的,简化后的结构体如下所示:
struct PyLongObject { long ob_refcnt; // 8 bytes struct _typeobject *ob_type; // 8 bytes long ob_size; // 8 bytes unsigned int ob_digit[1]; // 4 bytes * abs(ob_size) };
ob_refcnt引用计数8个字节,ob_type类型信息8个字节(指针),ob_size变长部分元素的个数8个字节。ob_digit变长的数据部分,字节数为4 * abs(ob_size),ob_size可以为0,所以ob_digit这部分可以占0字节,那么最少int就为8 + 8 + 8 = 24个字节,每次增量都是4(unsigned int)的倍数。
对于32位的版本与64位又有所不同,定义如下,最少12个字节,增量为2个字节。
struct PyLongObject { int ob_refcnt; // 4 bytes struct _typeobject *ob_type; // 4 bytes int ob_size; // 4 bytes unsigned short ob_digit[1]; // 2 bytes * abs(ob_size) };
至于其他类型实际大小,也是一个类似的方案,这里也不探讨太多东西了,学无止境吧~
总结
这篇文章从变量的角度切入,首先谈谈什么是变量的类型,并且举例了python中常用的基本数据类型,接着讨论了变量在内存中的存储,说白了就一句话,变量就是某一个对象的引用,对象在内存爱怎么放怎么放与变量无关。最后讨论了int类型占有电脑的字节数。
Tips:本人能力有限,如有错误之处麻烦指出。放弃不难,但坚持一定很酷!
到此这篇关于深入理解Python变量的数据类型和存储的文章就介绍到这了,更多相关Python变量的数据类型和存储内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!