浅谈Python3中函数参数传递方式
之前在看北理工嵩天等老师的python3的课程,在第五周中老师讲到了函数的调用传递。老师讲了这样一个例子
#处理多个银行账户的余额信息
def addInterest(balances, rate):
for i in range(len(balances)):
balances[i] = balances[i] * (1+rate)
def test():
amounts = [1000, 105, 3500, 739]
rate = 0.05
addInterest(amounts, rate)
print(amounts)
test()
在这个例子中可以看到为了处理多个银行账户设置了amounts这个列表,老师的原话是这样的:
“在test函数中一开始设置了amounts为4个值的列表,然后将amounts作为第一个参数传递给函数addInterest,并调用执行该函数,最后打印输出amounts的结果,运行结果如下:
[1050.0,110.25,3675.0,775.95]
”
然后礼欣老师得出结论,以下为原话
“在这个例子中可以看到,amounts变量的值好像是被修改了,但是函数是不能改变变量本身即amounts的”
①
接下来是分析下函数的执行过程,过程如下图
分析原话如下
“接下来分析下整个执行过程,查看amounts输出结果是如何发生变化的。首先函数test的前两条语句建立了两个变量amounts和rate,然后控制传递给addinterest,这里amounts是一个包含4个整数类型值的列表对象,以实参的形式传递给函数addinterest形参balances,下一步执行函数addinterest,从0到length-1范围执行循环,并更新balances的值。”
重点来了:原话如下
“图中旧值 [1000, 105, 3500, 739]
并没有改变,只是Python又创建了一组新值[1050.0,110.25,3675.0,775.95]
,并且使列表对象指向该组新值,而旧值会在Python的垃圾数据回收的时候被清除掉③,从图中我们可以清楚的看出,为什么包含列表参数的程序addinterest修改了列表的值?但程序addinterest结束时存储在amounts中的是新balances的值,实际上变量amounts从来没有被改变过。”
“它(amounts,作者注)仍然指向的是调用addinterest函数之前的同一个列表,只是当控制返回到调用函数中时,列表呈现了被修改的状态”②
最后是得出结论,原话如下:
"通过上述过程我们可以了解到:Python的参数是通过值来传递的。但是如果变量是可变对象,比如是列表或者是图形对象,返回到调用程序后,该对象会呈现出被修改的状态。"
_
注:课程原始视频部分结束。
看了老师的这段讲解之后产生了很多疑问:在前面(①处)讲的amounts是不能被修改的,但是在(②处)又说列表呈现了被修改的状态,这不是自相矛盾吗?在(③)处讲列表创建了新值并且使列表指向了新值,这里不就是说amounts发生了改变吗?怎么能说没变呢?最后结论也是列表呈现出了被修改的状态。这个结论云山雾绕,看得人似懂非懂。
那在Python3中参数变量是列表,在调用函数完返回后到底是被修改了还是没被修改呢?
为了弄清这个问题,我做了一个实验,id()可以查看变量在内存中的地址,这个值相当于一个对象的“身份证”。
# 处理多个银行账户的余额信息
def addInterest(balances, rates):
print()
print("第二处", id(balances))
for i in range(len(balances)):
balances[i]= balances[i]*(1+rates)
print()
print("第三处",id(balances))
def test():
amounts = [1000,105,3500,739]
print()
print("第一处",id(amounts))
rate = 0.05
addInterest(amounts, rate)
print()
print(amounts)
print()
print("第四处",id(amounts))
test()
输出结果:
第一处 41203656
第二处 41203656
第三处 41203656
第三处 41203656
第三处 41203656
第三处 41203656
[1050.0, 110.25, 3675.0, 775.95]
第四处 41203656
在这个实验中可以清楚的看到,amounts这个对象的身份证号码在整个程序运行过程中从未变过,而非视频中老师讲的创建了新的列表对象。所以amounts作为一个列表对象在程序运行过程中是被直接修改了,是的就是直接被修改了,而非指向新balances的值。为什么可以得出这一结论?我们可以看下第一、三处的id,在未进入函数之前id是41203656(第一处),进入函数之后对象id仍然未变,函数运行完返回之后对象id仍然未变!
所以结论应该这样写会比较清楚:
改变参数值值的函数:
实参传给形参时,python的参数是通过值来传递的;
如果变量是可变对象(如列表或者图形对象),该对象会在函数中会被直接修改,返回到调用程序后也是被修改后的状态。
那是不是Python3中函数都是像这种传递方式呢?我们对课程视频中的另一个例子做一个简单的修改。
# 计算单个银行账户余额
def addinterest(balance, rate):
print("第二处", id(balance))
newBalance = balance * (1 + rate)
print()
print("第三处", id(balance))
print()
print("第四处", id(newBalance))
return newBalance
def main():
amount = 1000
print("第一处", id(amount))
print()
rate = 0.05
amount = addinterest(amount, rate)
print()
print("第五处", id(amount))
print()
print(amount)
print("第六处", id(amount))
main()
运行结果如下:
第一处 33533648
第二处 33533648
第三处 33533648
第四处 33563344
第五处 33563344
1050.0
第六处 33563344
不是说好的直接修改的吗?怎么身份证又变了?其实这里的对象amount是个常数,即为不可变对象,当在函数中要对对象进行处理时,由于对象不可变,只能新建一个新对象,然后return出新的对象了。
这个也就是目前网络上大部分博客的结论:
1、不可变对象作为函数参数,Python通过值传递;
2、 可变对象作为函数参数,Python通过引用传递。
注:Python中,数值类型(int和float)、字符串str、元组tuple都是不可变类型。而列表list、字典dict、集合set是可变类型。
(但是也有博客把这两个结论搞反了)
但是也有博客提出了一个类似这样的例子
def change(val):
val = val + [10]
nums = [0, 1]
change(nums)
print(nums)
输出结果为
[0, 1]
其实这里是写的不严谨,不能直接用加号添加列表元素
可以改为这样
def change(val):
newval = [10]
val= val + newval
nums = [0, 1]
change(nums)
print(nums)
但是输出结果还是
[0, 1]
难道上面的结论不对吗?
其实这里要补充另外一种情况:对于可变对象作为函数参数,且参数不指向其他对象时,相当于引用传递;否则,若参数指向其他对象,则对参数变量的操作并不影响原变量的对象值
函数里的参数变量val指向了与nums不同的内存变量,所以函数里的参数变量val不影响原变量nums的值
这也是因为python的特性" 变量无类型,对象有类型 "。
变量是对内存空间的引用,当参数变量和原变量指向不同的内存空间时,操作互不影响。
用下面这个看下
def change(val):
newval = [10]
print("第二处",id(val))
val = val + newval
print("第三处",id(val))
nums = [0, 1]
print("第一处",id(nums))
change(nums)
print("第四处",id(nums))
print(nums)
运行结果如下:
第一处 39695944
第二处 39695944
第三处 39710024
第四处 39695944
[0, 1]
可以看到第一处的nums和第二处的val的内存地址完全一样,然后执行到第三处时,由于函数内VAL重新指向了别的内存变量,所以内存地址不同。但是最后结果要输出变量nums,即第一处第二处内存地址的值,所以和第三处的val就没关系了。其实这里的val是没有返回值的。
想要直接在列表中添加元素可以写成这样:
def change(val):
val.append(10)
nums = [0, 1]
change(nums)
print(nums)
输出结果是
[0, 1, 10]
关于变量无类型,对象有类型可以这样理解:只有放在内存空间中的对象(也就是数据)才有类型,而变量是没有类型的。
如果还是不明白可以做这样一种比喻:变量就好比钓鱼者,湖水就好像内存,里面有各种各样的鱼,它们就是对象。钓鱼者(变量)的任务就是用某种方式把自己和鱼(对象)通过鱼线连接起来。那么,鱼(对象)是有类型的,有鲢鱼、鲫鱼、带鱼。钓鱼者(变量)没有类型,他钓到不同类型的鱼(对象)。
用钓鱼的比喻解释下上面的例子
def change(val):
newval = [10]
val= val + newval
nums = [0, 1]
change(nums)
print(nums)
1、钓鱼人已经钓了一桶鱼用nums桶装着,nums桶可以装很多鱼。
2、现在提着这个nums桶继续在湖里钓鱼,这时候nums桶暂时叫做装鱼桶val,突然钓鱼人钓了一条大鱼,发现装鱼桶val装不下,于是钓鱼人又在渔具店买了另一个大的装鱼桶VAL,把大鱼和之前的鱼一块装了。
钓鱼活动结束。
3、最后要看看那个叫nums的桶有哪些鱼,这时候当然只能看之前的情况。
即这个结论:对于可变对象作为函数参数,且参数不指向其他对象时,相当于引用传递;否则,若参数指向其他对象,则对参数变量的操作并不影响原变量的对象值。
同样的针对其他两个结论,也可以用这个比喻解释:
def change( val):
newval = val + 10
return newval
num = 1
num = change(num)
print(num)
1、钓鱼人手上的东西num是个小蚯蚓。 2、钓鱼人拿着num去湖边钓鱼,小蚯蚓被大鱼吃了,钓鱼人钓到了一条大鱼,钓鱼人拿着鱼回家。 钓鱼活动结束。 3、问钓鱼人手上现在拿着什么东西num?当然是一条大鱼。
def change(val):
val.append(10)
nums = [0, 1]
change(nums)
print(nums)
1、钓鱼人提着一个叫nums的桶,桶里装着2条鱼 2、钓鱼人来到湖边钓鱼,此时桶暂时叫装鱼桶val,钓鱼人钓到了一条鱼放进装鱼桶val。
钓鱼活动结束。
3、看看钓鱼人桶里的有几条鱼。
总结来说:
对于不可变对象作为函数参数,相当于C系语言的值传递;
对于可变对象作为函数参数,且参数不指向其他对象时,相当于C系语言的引用传递。
对于可变对象作为函数参数,参数指向其他对象,对参数变量的操作不影响原变量的值。