参数传递

之前在看北理工嵩天等老师的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()  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这个例子中可以看到为了处理多个银行账户设置了amounts这个列表,老师的原话是这样的:

“在test函数中一开始设置了amounts为4个值的列表,然后将amounts作为第一个参数传递给函数addInterest,并调用执行该函数,最后打印输出amounts的结果,运行结果如下:

[1050.0,110.25,3675.0,775.95] 
  • 1

然后礼欣老师得出结论,以下为原话

“在这个例子中可以看到,amounts变量的值好像是被修改了,但是函数是不能改变变量本身即amounts的” 

这里的背景色是

接下来是分析下函数的执行过程,过程如下图 
addinterest()函数.png-267.9kB

分析原话如下

“接下来分析下整个执行过程,查看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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

输出结果:

第一处 41203656

第二处 41203656

第三处 41203656

第三处 41203656

第三处 41203656

第三处 41203656

[1050.0, 110.25, 3675.0, 775.95]

第四处 41203656
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这个实验中可以清楚的看到,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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

运行结果如下:

第一处 33533648

第二处 33533648

第三处 33533648

第四处 33563344

第五处 33563344

1050.0
第六处 33563344
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

不是说好的直接修改的吗?怎么身份证又变了?其实这里的对象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)
  • 1
  • 2
  • 3
  • 4
  • 5

输出结果为

[0, 1]
  • 1

其实这里是写的不严谨,不能直接用加号添加列表元素 
可以改为这样

def change(val):
    newval = [10]
    val= val + newval

nums = [0, 1]
change(nums)
print(nums)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

但是输出结果还是

[0, 1]
  • 1

难道上面的结论不对吗? 
其实这里要补充另外一种情况:对于可变对象作为函数参数,且参数不指向其他对象时,相当于引用传递;否则,若参数指向其他对象,则对参数变量的操作并不影响原变量的对象值

函数里的参数变量val指向了与nums不同的内存变量,所以函数里的参数变量val不影响原变量nums的值 
3.png-23.8kB
**这也是因为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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

运行结果如下:

第一处 39695944
第二处 39695944
第三处 39710024
第四处 39695944
[0, 1]
  • 1
  • 2
  • 3
  • 4
  • 5

可以看到第一处的nums和第二处的val的内存地址完全一样,然后执行到第三处时,由于函数内VAL重新指向了别的内存变量,所以内存地址不同。但是最后结果要输出变量nums,即第一处第二处内存地址的值,所以和第三处的val就没关系了。其实这里的val是没有返回值的。

想要直接在列表中添加元素可以写成这样:

def change(val):
    val.append(10)
nums = [0, 1]
change(nums)
print(nums)
  • 1
  • 2
  • 3
  • 4
  • 5

输出结果是

[0, 1, 10]
  • 1

关于变量无类型,对象有类型可以这样理解:只有放在内存空间中的对象(也就是数据)才有类型,而变量是没有类型的。

如果还是不明白可以做这样一种比喻:变量就好比钓鱼者,湖水就好像内存,里面有各种各样的鱼,它们就是对象。钓鱼者(变量)的任务就是用某种方式把自己和鱼(对象)通过鱼线连接起来。那么,鱼(对象)是有类型的,有鲢鱼、鲫鱼、带鱼。钓鱼者(变量)没有类型,他钓到不同类型的鱼(对象)。

用钓鱼的比喻解释下上面的例子

def change(val):
    newval = [10]
    val= val + newval

nums = [0, 1]
change(nums)
print(nums)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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
  • 2
  • 3
  • 4
  • 5
  • 6
1、钓鱼人手上的东西num是个小蚯蚓。
2、钓鱼人拿着num去湖边钓鱼,小蚯蚓被大鱼吃了,钓鱼人钓到了一条大鱼,钓鱼人拿着鱼回家。
钓鱼活动结束。
3、问钓鱼人手上现在拿着什么东西num?当然是一条大鱼。
  • 1
  • 2
  • 3
  • 4
def change(val):
    val.append(10)
nums = [0, 1]
change(nums)
print(nums)
  • 1
  • 2
  • 3
  • 4
  • 5

1、钓鱼人提着一个叫nums的桶,桶里装着2条鱼 2、钓鱼人来到湖边钓鱼,此时桶暂时叫装鱼桶val,钓鱼人钓到了一条鱼放进装鱼桶val。 
钓鱼活动结束。 
3、看看钓鱼人桶里的有几条鱼。

总结来说:

**对于不可变对象作为函数参数,相当于C系语言的值传递; 
对于可变对象作为函数参数,且参数不指向其他对象时,相当于C系语言的引用传递。 
对于可变对象作为函数参数,参数指向其他对象,对参数变量的操作不影响原变量的值。**

链接:

https://blog.csdn.net/wps20001990/article/details/79394502

posted on 2018-09-15 16:51  AI大道理  阅读(176)  评论(0编辑  收藏  举报

导航