python函数调用时参数传递方式

python函数调用时参数传递方式

C/C++参数传递方式

对于C程序员来说,我们都知道C在函数调用时,采用的是值传递,即形参和实参分配不同的内存地址,在调用时将实参的值传给实参,在这种情况下,在函数内修改形参并不会影响到实参,但是这样带来一个问题,如果我们需要刻意地对实参进行修改,就不得不传递实参的指针到函数,然后在函数中修改指针指向的数据,以达到修改实参的目的。

后来,C++中引入了引用这个概念,即在函数定义时,在形参前加一个&符号,表示传递参数的引用,在写法上,除了多出一个&符号,其他部分和C中传值调用一样,但是实际确是达到了可以在函数内修改实参内容的目的。这种参数传递的方式被称为传引用。

python的参数传递

说完了C/C++的参数传递方式,那么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

输出结果:

[4,5]

可以看到,在上述代码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时,x的地址刚好从对象1的内存地址变成了对象5的内存地址。

Tips:
    对于C/C++程序员来说,这段代码并不好理解,变量的值被改变时,竟然是变量的地址变化而不是原变量地址上的值变化!而且,为什么系统会为字面值1和5分配内存空间,这在C/C++中是不存在的!  
    所以我们要从python变量内存角度来理解:对象1和对象5早就在内存中存在,而变量x先是指向1的标签,在赋值后变成了指向5的标签。

更多内存细节可以参考我另一篇博客:python变量的内存机制

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,这种机制在一定程度上可以提高资源的重复利用,但是对C/C++程序员来说无疑算是一个陷阱。

可变类型和不可变类型混合的情况

我们人为地将变量分为可变类型和不可变类型,然后分类讨论,以为就万事大吉了,但是实际情况总是复杂的,我们可以来看看下面的例子:

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)
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则是完全复制一个对象的所有内容。

好了,关于python函数调用时参数调用方式的讨论就到此为止了,如果朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

个人邮箱:linux_downey@sina.com
原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.
(完)

posted @ 2019-03-06 14:54  牧野星辰  阅读(44101)  评论(2编辑  收藏  举报