在python中,不管是直接的变量赋值,仍是参数传递,都是按照引用进行赋值的。html
在计算机语言中,有两种赋值方式:按引用赋值、按值赋值。其中按引用赋值也常称为按指针传值(固然,它们仍是有点区别的),后者常称为拷贝副本传值。它们的区别,详细内容参见:按值传递 vs. 按指针传递。python
下面仅解释python中按引用赋值的相关内容,先分析下按引用赋值的特别之处,而后分析按引用赋值是什么样的过程。数据结构
例如:函数
a = 10000 b = a >>> a,b (10000, 10000)
这样赋值后,b和a不只在值上相等,并且是同一个对象,也就是说在堆内存中只有一个数据对象10000,这两个变量都指向这一个数据对象。从数据对象的角度上看,这个数据对象有两个引用,只有这两个引用都没了的时候,堆内存中的数据对象10000才会等待垃圾回收器回收。测试
它和下面的赋值过程是不等价的:指针
a = 10000 b = 10000
虽然a和b的值相等,但他们不是同一个对象,这时候在堆内存中有两个数据对象,只不过这两个数据对象的值相等。code
对于不可变对象,修改变量的值意味着在内存中要新建立一个数据对象。例如:htm
a = 10000 b = a a = 20000 >>> a,b (20000, 10000)
在a从新赋值以前,b和a都指向堆内存中的同一个数据对象,但a从新赋值后,由于数值类型10000是不可变对象,不能在原始内存块中直接修改数据,因此会新建立一个数据对象保存20000,最后a将指向这个20000对象。这时候b仍然指向10000,而a则指向20000。对象
结论是:对于不可变对象,变量之间不会相互影响。正如上面从新赋值了a=20000,但变量b却没有任何影响,仍然指向原始数据10000。blog
对于可变对象,好比列表,它是在"原处修改"数据对象的(注意加了双引号)。好比修改列表中的某个元素,列表的地址不会变,仍是原来的那个内存对象,因此称之为"原处修改"。例如:
L1 = [111,222,333] L2 = L1 L1[1] = 2222 >>> L1,L2 ([111, 2222, 333], [111, 2222, 333])
在L1[1]
赋值的先后,数据对象[111,222,333]
的地址一直都没有改变,可是这个列表的第二个元素的值已经改变了。由于L1和L2都指向这个列表,因此L1修改第二个元素后,L2的值也相应地到影响。也就是说,L1和L2仍然是同一个列表对象[111,2222,333]
。
结论是:对于可变对象,变量之间是相互影响的。
当将段数据赋值给一个变量时,首先在堆内存中构建这个数据对象,而后将这个数据对象在内存中的地址保存到栈空间的变量中,这样变量就指向了堆内存中的这个数据对象。
例如,a = 10
赋值后的图示:
若是将变量a再赋值给变量b,即b = a
,那么赋值后的图示:
由于a和b都指向对内存中的同一个数据对象,因此它们是彻底等价的。这里的等价不只仅是值的比较相等,而是更深层次的表示同一个对象。就像a=20000和c=20000,虽然值相等,但倒是两个数据对象。这些内容具体的下一节解释。
在python中有可变数据对象和不可变数据对象的区分。可变的意思是能够在堆内存原始数据结构内修改数据,不可变的意思是,要修改数据,必须在堆内存中建立另外一个数据对象(由于原始的数据对象不容许修改),并将这个新数据对象的地址保存到变量中。例如,数值、字符串、元组是不可变对象,列表是可变对象。
可变对象和不可变对象的赋值形式虽然同样,可是修改数据时的过程不同。
对于不可变对象,修改数据是直接在堆内存中新建立一个数据对象。如图:
对于可变对象,修改这个可变对象中的元素时,这个可变对象的地址不会改变,因此是"原处修改"的。但须要注意的是,这个被修改的元素多是不可变对象,多是可变对象,若是被修改的元素是不可变对象,就会建立一个新数据对象,并引用这个新数据对象,而原始的那个元素将等待垃圾回收器回收。
>>> L=[333,444,555] >>> id(L),id(L[1]) (56583832, 55771984) >>> L[1]=4444 >>> id(L),id(L[1]) (56583832, 55771952)
如图所示:
数值对象是不可变对象,理论上每一个数值都会建立新对象。
但实际上并不老是如此,对于[-5,256]
这个区间内的小整数,由于python内部引用过多,这些整数在python运行的时候就事先建立好并编译好对象了。因此,a=2, b=2, c=2
根本不会在内存中新建立数据对象2,而是引用早已建立好的初始化数值2。
因此:
>>> a=2 >>> b=2 >>> a is b True
其实能够经过sys.getrefcount()
函数查看数据对象的引用计数。例如:
>>> sys.getrefcount(2) 78 >>> a=2 >>> sys.getrefcount(2) 79
对于小整数范围内的数的引用计数都至少是几十次的,而超出小整数范围的数都是2或者3(不一样执行方式获得的计数值不同,好比交互式、文件执行)。
对于超出小整数范围的数值,每一次使用数值对象都建立一个新数据对象。例如:
>>> a=20000 >>> b=20000 >>> a is b False
由于这里的20000是两个对象,这很合理论。可是看下面的:
>>> a=20000;b=20000 >>> a is b True >>> a,b=20000,20000 >>> a is b True
为何它们会返回True?缘由是python解析代码的方式是按行解释的,读一行解释一行,建立了第一个20000时发现本行后面还要使用一个20000,因而b也会使用这个20000,因此它返回True。而前面的换行赋值的方式,在解释完一行后就会当即忘记以前已经建立过20000的数据对象,因而会为b建立另外一个20000,因此它返回False。
若是是在python文件中执行,则在赞成做用域内的a is b
一直都会是True,而无论它们的赋值方式如何。这和代码块做用域有关:整个py文件是一个模块做用域。此处只给测试结果,不展开解释,不然篇幅太大了,如不理解下面的结果,可看个人另外一篇Python做用域详述。
a = 25700 b = 25700 print(a is b) # True def f(): c = 25700 d = 25700 print(c is d) # True print(a is c) # False f()
对于下面的赋值过程:
L1 = [1,2,3] L2 = L1
前面分析过修改L1或L2的元素时都会影响另外一个的缘由:按引用赋值。实际上,按引用是指直接将L1中保存的列表内存地址拷贝给L2。
再看一个嵌套的数据结构:
L1 = [1,[2,22,222],3] L2 = L1
这里从L1拷贝给L2的也是外层列表的地址,因此L2能够找到这个外层列表包括其内元素。
下面是深、浅拷贝的概念:
所谓第一层次,指的是出现嵌套的复杂数据结构时,那些引用指向的数据对象属于深一层次的数据。例如:
L = [2,22,222] L1 = [1,2,3] L2 = [1,L,3]
L和L1都只有一层深度,L2有两层深度。浅拷贝时只拷贝第一层的数据做为副本,深拷贝递归拷贝全部层次的数据做为副本。
例如:
>>> L=[2,22,222] >>> L1=[1,L,3] >>> L11 = copy.copy(L1) >>> L11,L1 ([1, [2, 22, 222], 3], [1, [2, 22, 222], 3]) >>> L11 is L1 False >>> id(L1),id(L11) # 不相等 (17788040, 17786760) >>> id(L1[1]),id(L11[1]) # 相等 (17787880, 17787880)
注意上面的L1和L11是不一样的列表对象,但它们中的第二个元素是同一个对象,由于copy.copy是浅拷贝,只拷贝了这个内嵌列表的地址。
而深拷贝则彻底建立新的副本对象:
>>> L111 = copy.deepcopy(L1) >>> L1[1],L111[1] ([2, 22, 222], [2, 22, 222]) >>> id(L1[1]),id(L111[1]) (17787880, 17787800)
由于是浅拷贝,对于内嵌了可变对象的数据时,修改内嵌的可变数据,会影响其它变量。由于它们都指向同一个数据对象,这和按引用赋值是同一个道理。例如:
>>> s = [1,2,[3,33,333,3333]] >>> s1 = copy.copy(s) >>> s1[2][3] = 333333333 >>> s[2], s1[2] ([3, 33, 333, 333333333], [3, 33, 333, 333333333])
通常来讲,浅拷贝或按引用赋值就是咱们所期待的操做。只有少数时候(好比数据序列化、要传输、要持久化等),才须要深拷贝操做,但这些操做通常都内置在对应的函数中,无需咱们手动去深拷贝。