Python中的不可变对象类型与可变对象类型
其实各个标准资料中没有说明Python有值类型和引⽤类型的分类,这个分类⼀般是C++和Java中的。但是语⾔是相通的,所以Python肯定也有类似的。实际上Python 的变量是没有类型的,这与以往看到的⼤部分语⾔都不⼀样(JS等弱类型的也是这样)。但 Python 却是区分类型的,那类型在哪⾥呢?事实是,类型是跟着内存中的对象⾛的。类型属于对象,变量是没有类型的。⼀般也分实参和形参。
《learning python》中的⼀个观点:变量⽆类型,对象有类型。
1. 对象类型
不可变(immutable)对象类型
int
float
decimal
complex
bool
str
tuple
range
frozenset
bytes
可变(mutable)对象类型
list
dict
set
bytearray
user-defined classes (unless specifically made immutable)
2. 变量
Python中的变量都是指针,这确实和之前学过的强类型语⾔是有不同的。因为变量是指针,所以所有的变量⽆类型限制,可以指向任意对象。指针的内存空间⼤⼩是与类型⽆关的,其内存空间只是保存了所指向数据的内存地址。
Python 的所有变量其实都是指向内存中的对象的⼀个指针,所有的变量都是!
此外,对象还分两类:⼀类是可修改的,⼀类是不可修改的。我的理解是把可修改(mutable)的类型叫做值类型,不可修改(immutable)类型叫做引⽤类型。
3. 对象
对象=确定内存空间+存储在这块内存空间中的值。
Java中,对象是分配在堆上的,存储真正的数据,⽽引⽤是在栈中开辟的内存空间⽤于引⽤某⼀个对象(值类型的变量也是存储到栈上)。
4. 值类型(不可变对象)
在Python中,数值(整型,浮点型),布尔型,字符串,元组属于值类型,本⾝不允许被修改(不可变类型),数值的修改实际上是让变量指向了⼀个新的对象(新创建的对象),所以不会发⽣共享内存问题。这种⽅式同Java的不可变对象(String)实现⽅式相同。原始对象被Python的GC回收。
a = 1
b = a
a = 2
print(b)  #输出的结果是1 修改值类型的值,只是让它指向⼀个新的内存地址,并不会改变变量b的值。
1 >>> x = 1
2 >>> id(x)
3 31106520
4 >>> y = 1
5 >>> id(y)
6 31106520
7 >>> x = 2
8 >>> id(x)
9 31106508
10 >>> y = 2
11 >>> id(y)
12 31106508
13 >>> z = y
14 >>> id(z)
15 31106508
16 >>> x += 2
17 >>> id(x)
18 31106484
对不可变数据类型中的int类型的操作,id()查看的是当前变量的地址值。
x = 1和y = 1两个操作的结果,从上⾯的输出可以看到x和y在此时的地址值是⼀样的,也就是说x和y其实是引⽤了同⼀个对象,即1,也就是说内存中对于1只占⽤了⼀个地址,⽽不管有多少个引⽤指向了它,都只有⼀个地址值,只是有⼀个引⽤计数会记录指向这个地址的引⽤到底有⼏个⽽已。
当我们进⾏x = 2赋值时,发现x的地址值变了,虽然还是x这个引⽤,但是其地址值却变化了,后⾯的y = 2以及z = y,使得x、y和z都引⽤了同⼀个对象,即2,所以地址值都是⼀样的。
当x和y都被赋值2后,1这个对象已经没有引⽤指向它了,所以1这个对象占⽤的内存,即31106520地址要被“垃圾回收”,即1这个对象在内存中已经不存在了。
最后,x进⾏了加2的操作,所以创建了新的对象4,x引⽤了这个新的对象,⽽不再引⽤2这个对象。
那么为什么称之为不可变数据类型呢?
这⾥的不可变⼤家可以理解为x引⽤的地址处的值是不能被改变的,也就是31106520地址处的值在没被垃圾回收之前⼀直都是1,不能改变,如果要把x赋值为2,那么只能将x引⽤的地址从31106520变为31106508,相当于x = 2这个赋值⼜创建了⼀个对象,即2这个对象,然后x、y、z都引⽤了这个对象,所以int这个数据类型是不可变的,如果想对int类型的变量再次赋值,在内存中相当于⼜创建了⼀个新的对象,⽽不再是之前的对象。从下图中就可以看到上⾯程序的过程。
备注:图⽚中错误。x=2,y=2,z=y。
从上⾯的过程可以看出,不可变数据类型的优点就是内存中不管有多少个引⽤,相同的对象只占⽤了⼀块内存,但是它的缺点就是当需要对变量进⾏运算从⽽改变变量引⽤的对象的值时,由于是不可变的数据类型,所以必须创建新的对象,这样就会使得⼀次次的改变创建了⼀个个新的对象,不过不再使⽤的内存会被垃圾回收器回收。
5. 引⽤类型(可变类型)
在Python中,列表,集合,字典是引⽤类型,本⾝允许修改(可变类型)。
1 list_a = [1,2]
2 list_b = list_a
3 list_a[0] = 3
4print(list_b)  #此时的输出结果是[3,2]
修改引⽤类型的值,因为list_b的地址和list_a的⼀致,所以也会被修改。
1 >>> a = [1, 2, 3]
2 >>> id(a)
3 41568816
4 >>> a = [1, 2, 3]
5 >>> id(a)
6 41575088
7 >>> a.append(4)
8 >>> id(a)
9 41575088
10 >>> a += [2]
11 >>> id(a)
12 41575088
13 >>> a
python中的字符串是什么14 [1, 2, 3, 4, 2]
从上⾯的程序中可以看出,进⾏两次a = [1, 2, 3]操作,两次a引⽤的地址值是不同的,也就是说其实创建了两个不同的对象,这⼀点明显不同于不可变数据类型,所以对于可变数据类型来说,具有同样值的对象是不同的对象,即在内存中保存了多个同样值的对象,地址值不同。
我们对列表进⾏添加操作,分别a.append(4)和a += [2],发现这两个操作使得a引⽤的对象值变成了上⾯的最终结果,但是a引⽤的地址依旧是41575088,也就是说对a进⾏的操作不会改变a引⽤的地址值,只是在地址后⾯⼜扩充了,改变了地址⾥⾯存放的值,所以可变数据类型的意思就是说对⼀个变量进⾏操作时,其值是可变的,值的变化并不会引起新建对象,即地址是不会变的,只是地址中的内容变化了或者地址得到了扩充。
可变数据类型是允许同⼀对象的内容,即值可以变化,但是地址是不会变化的。但是需要注意⼀点,对可变数据类型的操作不能是直接进⾏新的赋值操作,⽐如说a = [1, 2, 3, 4, 5, 6, 7],这样的操作就不是改变值了,⽽是新建了⼀个新的对象,这⾥的可变只是对于类似于append、+=等这种操作。
6. 不可变的例外
并⾮所有的不可变对象都是不可变的。
如前所述,Python容器⽐如元组,是不可变的。这意味着⼀个tuple的值在创建后⽆法更改。但是元组的“值”实际上是⼀系列名称,它们与对象的绑定是不可改变的。关键点是要注意绑定是不可改变的,⽽不是它们绑定的对象。
让我们考虑⼀个元组t =('holberton',[1,2,3])
上⾯的元组t包含不同数据类型的元素,第⼀个元素是⼀个不可变的字符串,第⼆个元素是⼀个可变列表。元组本⾝不可变。即它没有任何改变其内容的⽅法。同样,字符串是不可变的,因为字符串没有任何可变⽅法。但是列表对象确实有可变⽅法,所以可以改变它。这是⼀个微妙的点,但是⾮常重要:不可变对象的“值” 不能改变,但它的组成对象是能做到改变的。
其实主要原因是元组内保存的是变量(也就是内存地址)。所以当变量指向对象发⽣变化时,如果导致变量发⽣变化(即不可变类型),此时元组保证该不可变类型不能修改。⽽如果当变量指向对象发⽣变化时,如果不会导致变量发⽣变化(即可变类型),此时元组中存储的该可变类型可以修改(因为变量本⾝并⽆变化)。
7. 总结
python中的不可变数据类型,不允许变量的值发⽣变化,如果改变了变量的值,相当于是新建了⼀个对
象,⽽对于相同的值的对象,在内存中则只有⼀个对象,内部会有⼀个引⽤计数来记录有多少个变量引⽤这个对象;
可变数据类型,允许变量的值发⽣变化,即如果对变量进⾏append、+=等这种操作后,只是改变了变量的值,⽽不会新建⼀个对象,变量引⽤的对象的地址也不会变化,不过对于相同的值的不同对象,在内存中则会存在不同的对象,即每个对象都有⾃⼰的地址,相当于内存中对于同值的对象保存了多份,这⾥不存在引⽤计数,是实实在在的对象。
8. 参数传递
C++中是传值和传引⽤(指针)。c语⾔加上*号传递指针就是引⽤传递,⽽直接传递变量名就是值传递)
Java是传值(传值和传引⽤,只不过引⽤就是内存地址,所以也是值)。Java⾥区分值和引⽤,是因为值存储在栈⾥,⽽引⽤对象存储在堆⾥(引⽤本⾝在栈⾥)。
⽽Python所有的都是对象,都是引⽤,所以所谓的值类型都是不可变类型。类似于Java的字符串类型。
所以Python中的参数传递都是传递引⽤,也就是传递的是内存地址。只不过对于不可变类型,传递引⽤和传递值没什么区别。⽽对于可变类型,传递引⽤是真的传递内存的地址。
听说python只允许引⽤传递是为⽅便内存管理,因为python使⽤的内存回收机制是计数器回收,就是每块内存上有⼀个计数器,表⽰当前有多少个对象指向该内存。每当⼀个变量不再使⽤时,就让该计数器-1,有新对象指向该内存时就让计数器+1,当计时器为0时,就可以收回这块内存了。当然还有其他的GC⽅法,否则计数器回收,⽆法解决循环引⽤的问题。
值传递: 表⽰直接传递变量的值,把传递过来的变量的值复制到形参中,这样在函数内部的操作不会影响到外部的变量。
引⽤传递: 把引⽤理解为变量(标识符)与数据之间的引⽤关系,标识符通过引⽤指向某块内存地址。⽽引⽤传递,传递过来的就是这个关系,当你修改内容的时候,就是修改这个标识符所指向的内存地址中的内容,因为外部也是指向这个内存中的内容的,所以,在函数内部修改就会影响函数外部的内容。
另外:列表,字典,集合是容器类型,嵌套引⽤。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。