对内存的高效利用一直是程序编写过程中的一个亘古不变的话题,这一节我们就用实际操作来看看python3中各种变量的内存使用区别,以及它们在进行函数传参时候的注意事项。
内存中的堆栈
因为是动态语言,python中的所有变量内容都存在堆(heap)中,而变量名只是堆中内容的引用,存放在栈(stack)中,便于用户去间接操作堆中的数据。这对于后面理解函数参数的传递非常重要。
做为对比,以javascript为例,基本数据类型,例如数值、字符串、布尔值,直接存在于栈内;而复合数据类型,例如array、object,存在于堆内,栈内存放的是堆地址的引用
但是栈里面的变量名和堆里面的实际内容并不是一对一的关系,这与变量类型以及具体内容有关系。下面我们就用具体例子来依次实验下。
不同数据类型内存使用
首先必须要知道id()
命令可以用来查看变量在堆中的内存地址,同时==
只能用来比较两个变量值的大小,而is
可以同时比较内存地址和值。
不可变对象
python中的不可变对象包括:
- int
- float
- bool
- tuple
- string
之所以叫这5种为不可变对象,就是因为一旦变量名和堆中的某个地址绑定以后,再想修改该变量的值,就会重新再将该变量名和另一个堆地址进行绑定。换句话说,对于5种不可变对象,如果变量的值不同,内存地址一定不同。同一个变量修改内容以后内存地址一定改变。但是是不是对不同的变量分别赋值相同的内容,两个变量在堆中对应的地址就一定一样呢?这个就不一定了,下面来依次看看。
小整数和大整数
python中将介于-5到256的小整数在堆中分配了独立的缓存区,也就是说当变量引用这些值的时候,只要值相同,不管引用多少次内存地址一定相同。而对于另外区间的整数,即使是值相同,多次引用也会创造不同的内存地址。
In [1]: int1=1
In [2]: int2=1
In [3]: id(int1)
Out[3]: 94569156809600
In [4]: id(int2)
Out[4]: 94569156809600
In [5]: int3=123456
In [6]: int4=123456
In [7]: id(int3)
Out[7]: 140692865269264
In [8]: id(int4)
Out[8]: 140692864485680
所以,对于大整数,即使值相同,不同的调用内存地址也不同。
短字符串和长字符串
python中对于没有空格的字符串认定为短字符串,类似于小整数,只要内容相同,不管引用多少次地址都一样。而带了空格的,即使内容相同,多次引用的地址也不同
In [1]: str1='dfkdjf'
In [2]: str2='dfkdjf'
In [3]: id(str1)
Out[3]: 140645071595648
In [4]: id(str2)
Out[4]: 140645071595648
In [5]: str3='dfkdjf rrr'
In [6]: str4='dfkdjf rrr'
In [7]: id(str3)
Out[7]: 140645018745904
In [8]: id(str4)
Out[8]: 140645018373744
需要注意的是,如果是中文,不管有没有空格,地址都是不一样的
In [9]: str5='我是小付'
In [10]: str6='我是小付'
In [11]: id(str5)
Out[11]: 140645017829736
In [12]: id(str6)
Out[12]: 140645017830568
所以,对于长字符串,即使值相同,不同的调用内存地址也不同。
浮点数
浮点数并没有短长的区分,不同的引用地址一定不同
In [1]: f1=1.23
In [2]: f2=1.23
In [3]: id(f1)
Out[3]: 139760803510072
In [4]: id(f2)
Out[4]: 139760803510144
元组
元组和浮点型一样,地址不同
In [5]: tup1=(1,2,3)
In [6]: tup2=(1,2,3)
In [7]: id(tup1)
Out[7]: 139760792784472
In [8]: id(tup2)
Out[8]: 139760801764336
布尔值
布尔值一共就两个,所以相同的值在内存中的地址是不变的
In [9]: b1=True
In [10]: b2=True
In [11]: id(b1)
Out[11]: 94256849978176
In [12]: id(b2)
Out[12]: 94256849978176
可变对象
python中的可变对象包括:
- list
- dict
- set
之所以是可变对象,是因为一旦一个变量和堆中的某个地址绑定,即使修改变量的内容,堆中的地址也不会变了。所以对于3种可变对象,不管值是否相同,不同变量对应的内存地址一定不同,但是同一变量对应的内存地址一定不变。
如下,即使值相同,内存地址也不同
In [13]: set1={1,2,3}
In [14]: set2={1,2,3}
In [15]: id(set1)
Out[15]: 139760793306952
In [16]: id(set2)
Out[16]: 139760793308520
In [17]: dic1={}
In [18]: dic2={}
In [19]: id(dic1)
Out[19]: 139760801165384
In [20]: id(dic2)
Out[20]: 139760800828368
In [22]: list1=[1,2]
In [23]: list2=[1,2]
In [24]: id(list1)
Out[24]: 139760801034760
In [25]: id(list2)
Out[25]: 139760800576904
而同一变量,即使修改了内容,内存地址还是不变
In [1]: list1=[1,2]
In [2]: id(list1)
Out[2]: 139780650584328
In [3]: list1.append(3)
In [4]: id(list1)
Out[4]: 139780650584328
综合以上的所有例子,可以总结如下
-
真实数据都是存储在堆中,栈中的变量都只是存储堆中数据的引用
-
不可变对象中的小整数、短字符串和布尔值,只要值相同内存地址就相同。其余类型不管值是否相同内存地址都不同
-
可变对象对于单个变量不管值如何变,内存地址都是固定的。但是不同变量,不管值是否相同,内存地址都不同
匿名对象
但是要特别说明以下的就是直接使用数值的情况,比较玄幻
In [14]: id([1,2])
Out[14]: 139780420063624
In [15]: id([3,4])
Out[15]: 139780677582280
In [16]: id([1,2])==id([3,4])
Out[16]: True
据说python为匿名可变对象在堆中开了统一的一块内存地址,所以暴露的地址都是一致的。这里我们就不深究了,毕竟这种情况看内存地址的可能性不大。
变量名赋值给变量
既然python中的变量存储的都是堆中数据的地址,就类似于指针,所以将变量名赋值给另一个变量就相当于将新的变量指向了同一个内存地址。至于修改值以后两个变量的值如何改变,只需要根据上面数据类型的内存地址变换规律去推就好了。
举几个例子。
不可变对象中的整数
In [17]: a=123
In [18]: b=a
In [19]: id(a)
Out[19]: 94805275202240
In [20]: id(b)
Out[20]: 94805275202240
In [21]: b
Out[21]: 123
这里变量a
和b
指向同一个地址,之后修改a
的值,根据前面的规律,同一变量修改内容后的内存地址不一样
In [22]: a+=1
In [23]: id(a)
Out[23]: 94805275202272
In [24]: id(b)
Out[24]: 94805275202240
所以变量a
的值变了,而b
得值维持不变
In [25]: a
Out[25]: 124
In [26]: b
Out[26]: 123
可变对象中得列表
In [27]: c=[1,2,3]
In [28]: d=c
In [29]: id(c)
Out[29]: 139780440014600
In [30]: id(d)
Out[30]: 139780440014600
之后修改c
得内容,根据上面得规律,c
得地址不会变,还是指向源地址,所以c
和d
得值都改变了
In [31]: c.append(4)
In [32]: id(c)
Out[32]: 139780440014600
In [33]: id(d)
Out[33]: 139780440014600
In [34]: c
Out[34]: [1, 2, 3, 4]
In [35]: d
Out[35]: [1, 2, 3, 4]
函数传参
理解了上面得变量名赋值,再来看函数传参就简单了。因为传参都是将栈中得变量内容传递到函数内,相当于传递一个指针到函数内,所以在函数内对变量得操作也分为可变对象和不可变对象两种情况。
如果传递的是不可变对象,在函数内得修改只是将变量指向了另一个地址,所以不会影响函数外得变量内容;如果传递得是可变对象,在函数内得修改还是在原地址进行修改,所以还会影响到函数外的变量内容。
补充:用javascript做为对比,js中的基础变量都是直接将值保存在栈内,所以相当于传值给函数,函数内的操作不会影响函数外的变量值。而js中的复合变量同样也是将值保存在堆内,所以相当于传递指针,函数内的操作也会影响到函数外的变量值
总结
总结下知识点
-
真实数据都是存储在堆中,栈中的变量都只是存储堆中数据的引用
-
不可变对象中的小整数、短字符串和布尔值,只要值相同内存地址就相同。其余类型不管值是否相同内存地址都不同
-
可变对象对于单个变量不管值如何变,内存地址都是固定的。但是不同变量,不管值是否相同,内存地址都不同
-
传参的时候都是传指针,根据修改内容是否改变内存地址来看看是否会影响到外部变量,不可变对象不影响,可变对象会影响
-
做为对比的javascript,基础变量是传值,函数内修改不影响外部变量,复合变量是传指针,函数内修改会影响外部变量
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。