python函数调用时--参数传递方式
python的参数传递
python中参数传递到底是传值还是传引用呢?
test1.py:
def test(num): num += 10 x = 1 test(x) print x
输出结果:1
test2.py:
def test(lst): lst[0] = 4 lst[1] = 5 tlist = [1,2] test(tlist) print tlist
在上述代码test1.py中,在函数中修改传入的x的值,函数执行完之后,x并没有改变,至少对于int型变量而言,python函数调用为传值。
在代码test2.py中,在函数中修改传入的tlist的值,函数执行完,list的内容却被函数修改了,从这里又可以看出,对于list类型而言,python函数调用为传引用。
所以,python的函数调用到底是传值还是传引用?
python的变量内存模型
要搞清楚python的函数调用时传值还是传引用,这还得从python的变量内存模型说起,作为一个C/C++程序员,对于变量的理解就是CPU会为每个变量分配独立的内存空间,在变量生存周期结束时内存空间被收回。
但python却使用了另一种完全不同的机制,对于python而言,一切皆对象,python为每个对象分配内存空间,但是并非为每个变量分配内存空间,因为在python中,变量更像是一个标签,就像在现实生活中,一个人可以有多种身份标签,比如:XX的父亲,XX的儿子,XX的工程师,X地志愿者等等,但对应的实体都是同一个人,只占同一份资源。
x = 1 print(id(x)) print(id(1)) print(id(5)) x= 5 print(id(x)) print(id(1)) print(id(5))
输出:
166656176 166656176 166656128 166656128 166656176 166656128
当我们将x变量的值由1变成5时(1和5是两个不同的对象,x变量前后指向了两个不同对象得地址),x的地址刚好从对象1的内存地址变成了对象5的内存地址。
python的间接引用机制
可变类型和不可变类型
在python中将类型分为了可变类型和不可变类型,分别有:
可变数据类型:列表,字典 (可变数据类型是某数据在内存中修改不产生新的对象,即修改后数据的内存地址不变)
不可变数据类型:int、float、string、tuple (不可变数据类型是指某数据在内存中修改产生新的对象,即数据的内存地址改变)
简单地来理解可变类型和不可变类型:在修改该类型变量时是否产生新对象,如果是在原对象上进行修改,为可变对象,如果是产生新的对象,则是不可变对象。
那么怎么判断是否产生新的对象呢?我们可以用python内建id()函数来判断,这个函数返回对象在内存中的位置,如果内存位置有变动,表明变量指向的对象已经被改变。
python传参时可变类型和不可变类型的区别
事实上,对于python的函数传递而言,我们不能简单地用传值或者传址来定义参数传递,我们从上一部分中可变类型和不可变类型的角度来分析:
- 在参数传递时,实参将标签复制给了形参,这个时候形参和实参都是指向同一个对象。
- 在函数内修改形参(实参传递给形参):
- 对于不可变数据类型变量而言:因为不可变数据类型变量特性,修改变量需要新创建一个对象,形参的标签转而指向新对象,而实参没有变
- 对于可变数据类型变量而言,因为可变数据类型变量特性,直接在原对象上修改,因为此时形参和实参都是指向同一个对象,所以,实参指向的对象自然就被修改了。
到这里,应该就不难理解为什么在"python的参数传递"部分,test1.py和test2.py执行完两种完全不同的结果了(test1.py传入不可变类型int,实参未被函数修改。而test2.py传入可变类型list,实参被修改)。
不仅仅是参数传递
在上面我们谈论了可变参数和不可变参数在参数传递时的行为,其实,这种机制存在于整个python环境中,而不仅仅是参数传递中,我们看下面的例子:
list1 = [1,2] list2 = list1 list1[0] = 3 print list1 print list2
输出:
[3, 2]
[3, 2]
在上述示例中,令list2 = list1,因为根据之前的理论,list2和list1指向同一个对象(指向同一个对象的内存地址),所以修改list1时同时也会修改list2,这种机制在一定程度上可以提高资源的重复利用。
可变类型和不可变类型混合的情况
我们人为地将变量分为可变类型和不可变类型,然后分类讨论,以为就万事大吉了,但是实际情况总是复杂的,我们可以来看看下面的例子:
lst = [(1,2),3]
tup = ([1,2],3)
像这种情况中,两种类型糅杂在一起,那怎么去区分他们到底属于哪个阵营呢?这样的变量在修改时会不会创建新的对象呢?我们再来试一试:
tup = ([1,2],3) tup1 = tup tup1[0][0] = 3 print tup print tup1
输出结果:
([3,2],3)
([3,2],3)
这个结果就很有意思了,元组的第一个元素为一个列表,且令tup1 = tup,即两个变量指向同一个对象,我们修改了元组的第一个元素(列表)的第一个元素,结果两个元组的数据都变了。由此引出两个问题:
- 为什么元组内的数据可以修改?
- 作为一个不可变类型变量,为什么我们修改元组的成员时,不会创建一个新的对象而是在原对象上修改,导致另一个指向这个对象的变量取值也发生了变化?
对于这个问题,我们可以这样去理解:在定义tup变量时,此前内存中没有tup对象,所以系统需要新建一个tup对象,对于tup[0]即[1,2],系统会再去找是否存在这样的对象,如果存在,直接引用原对象,如果不存在,再创建新的对象,对于tup[1]即3,也是同样的道理。
所以,事实上,tup变量只是一个引用,tup[0]同时也只是个引用,tup[1]照样如此,所以,创建一个符合类型的变量并非我们所想的在内存中专门开辟一片区域来放置整个对象,而是在不断地引用其他对象。
所以,对于问题1,为什么元组内的数据可以修改?答案是,整个元组并非一个统一的整体,我们修改的是tup[0]的元素,即一个列表变量,自然可以修改,
注意:[0,1]这个整体属于元组的元素,是不可修改的,即我们不能将[0,1]整体替换成其他,但是单独的0,1属于列表的元素,是可以修改的。
对于问题2,既然我们修改的列表的元素,列表是可变参数类型,那么自然在原对象上修改而非创建新对象。
现实中的各种情况
上面说到了python传递参数的特性,那么如果我们要在函数中修改一个不可变对象的实参,又或者是在函数中不修改可变类型的实参,那该怎么做呢?
首先,如果要在函数中修改一个不可变参数的实参,最简单也最实用的办法就是传入这个参数同时返回这个参数,因为虽然是同一个变量,在传入和返回时这个变量已经指向了不同的对象:
def test(num): #num参数指向对象5 mum += 10 #num参数指向新对象15 return num #返回num,此时num为15 print test(5)
然后,如果要在函数中不修改可变参数的实参,这个时候就需要引用另一个模块:copy,就是重新复制出另一个可变类型参数,我们可以看下面的例子:
import copy lst = [1,2] lst_cp = copy.copy(lst) #copy产生出了一个新的对象 print id(lst) print id(lst_cp)
输出结果:
3072211148 3072211340
从上述示例可以看出,copy过程中系统复制了一个新的对象,而不是简单地引用原来对象(两个变量指向数据地址不一样)。
但是需要注意的是,copy只对可变类型变量才创建新的对象,而对不可变类型变量,并不创建新的对象,大家可以去试试。
copy复制依然可能存在问题
在可变类型和不可变类型混合的情况下,我们知道了,一个变量很可能并非仅仅指向一个完整的对象,变量的子元素依然存在引用的情况,比如在lst = [[1,2],3)]中,lst[0]就是引用了别处的对象,而非在内存中完全存在一个单独的[[1,2],3]对象,那么,如果使用copy对lst进行复制,对于lst[0],是仅仅复制了引用,还是复制了整个对象呢?我们可以看下面的例子:
import copy lst = [[1,2],3] lst_cp = copy.copy(lst) print id(lst) print id(lst_cp) print id(lst[0]) print id(lst_cp[0])
输出结果:
3072042988 3072043404 3072043020 3072043020
从结果可以看出,对于lst列表,仅仅是复制了lst对象,lst[0]仅仅是复制了引用。这其实违背了我们的本意,如果我们要完整地复制整个对象,那又改怎么做呢?
deepcopy
对于上面问题的解决方案是:使用copy模块的deepcopy方法,相对于copy方法(通常被称为浅拷贝),deepcopy(通常被称为深拷贝),浅拷贝就像上述的例子一样,复制出一个新的对象,但是目标对象中的子对象可能是引用。而deepcopy则是完全复制一个对象的所有内容。