python 变量: 引用和可变性
1. 再谈 python 变量:引用和可变性
学习任何一种语言都逃避不了这样一个触及灵魂的问题:值传递还是引用传递?这是一个很重要的问题,因为你有可能改变了不该改变的对象。
在 python 中,任何一个变量包括三个属性:标识,类型和值,类型很好理解,标识通常指内存中的地址,值是指变量存储的值。
1.1 变量赋值到底发生了什么?
上面罗列了 python 中常见的数据类型,有列表,集合、字典,元组,数值和字符串以及类对象,其中列表,集合、字典和类对象是可变对象,元组和数值和字符串是不可变对象。
python 中可以通过id
来查看变量的内存地址(Cpython 编译器中),因此可以来判定是否内存中是同一个地址变量。
我们一起来用 id 探究下变量的赋值情况
变量的初始化赋值,会在内存在开辟一个空间地址存储变量,正常的情况,每个变量应该 id 都不一样,有例外吗?
下面是符合预期的,两个 list(alist 和 blist)变量都是不一样,但是变量的值是一样,因此 == 是成立的。
当用变量给另一个变量赋值值,传递的是引用,如 clist 和 alist 都是指向同一个内存地址,id 是一样的,可以认为 clist 是 alist 的一个别名,实质是同一个对象,因此修改 clist,alist 的内存也会跟着改变。以此类推 list,set,dict 和 tuple 都是一样
一个例外我们来看看数值和字符串有什么不一样的
从上面的例子可以看出
浮点型的并没有特殊之处
比较小整形如 3,虽然申明了两个变量,但是地址也是一样。是不是有点奇怪,其实这个 python 为了比较常用的类型做的内存优化,python 解释器会在启动时创建出小整数池,范围是[-5,256],该范围内的小整数对象是全局解释器范围内被重复使用,永远不会被垃圾回收机制回收。
比较大的整形,超出范围按照常规方式
字符串也和比较小整形的方式一样的。
1.2 变量的可变性
上一小节说到 python 中列表,集合、字典和类对象是可变对象,元组和数值和字符串是不可变对象。
可变对象即对象值发生变化时,地址没有发生变化
不可变对象是对象值发生变化时,地址也发生了变化,相当于重新创建了一个变变量
我们拿列表 List 和元组 tuple 来举个例子:
从上面的例子可以知道:
列表的自我修改不会给变内存地址, 而元组地址变了
需要注意的是: + 运算和 += 对于 List 的区别:+= 对于可变对象是自我修改操作,而对于不可变对象则是运算和+是一样的,这样就是重新创建一个新的变量
那么不可变对象的元素是不可以修改的吗?我们拿元组的相对不可变来说明下:
元组的相对不可变性:如果元组中元素是可变, 元组中的可变元素是可以修改的
从上可知,元组中可变对象值发生了变化,元组的引用地址没有变。
1.3 深复制和浅复制
由于变量的赋值,只是对是同一个对象标识建立一个别名,本质上是一个引用。所以当你想重新开辟一个空间时,要特别的注意;特别是多种不同对象嵌套的情况下,因为结果并不是你所预期的那样完全新的对象,可能相互影响,导致不可预见的 bug。
这也是需要了解 python 引用的原因。
copy 模块提供了对象的拷贝,通常情况是浅拷贝,对于单一对象通常是可以的。对象.copy()
也可以达到一样的结果(依赖对象的具体实现)。
所谓浅拷贝,只拷贝第一层级的变量,但是如果是第一层级的变量是可变对象(引用对象),浅拷贝只会拷贝引用地址,这个时候改变这个可变对象,会同时影响两个父变量,这个时候需要深拷贝。
从上可知,虽然 alist 和 clist 是不同的对象,但是 alist 中可变对象值发生变化时会影响到 clist。
如果改成深度 copy 则可以解决,如下:
1.4 函数传参
python 中函数传参是共享传参,就是指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。
这种情况下,函数可能会修改作为参数传入的任何可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。
从上面可知,对于不可变对象 +=是重新建立了新对象引用标识,不影响外部的变量的值。而对于可变对象,函数内部的修改也影响到外部的变量的值(共同的引用)
而上面这个例子,因为+的运算符是构建新的变量所以都不会改变外部的值。所以需要清除你是否需要改变外部变量的值。
版权声明: 本文为 InfoQ 作者【AIWeker-人工智能微客】的原创文章。
原文链接:【http://xie.infoq.cn/article/bd1fd04ee45035087e7dac798】。文章转载请联系作者。
评论