********************************************************************
*****摘自<编写高质量代码 改善Python程序的91个建议>*****
********************************************************************
Python中的函数参数到底是传值还是传引用?这个许多人在学习过程中会纠结的一个问题,总结有如下3点:
- 传引用
- 传值
- 可变对象传引用,不可变对象传值
以下依次举一个实际示例验证这几个观点
1)传引用
1 >>> def inc(n): 2 ... print(id(n)) 3 ... n = n + 1 4 ... print(id(n)) 5 ... 6 >>> n = 3 7 >>> id(n) 8 10910464 9 >>> inc(n) 10 10910464 11 10910496 12 >>> print(n) 13 3 14 >>>
按照传引用的概念,上面的例子期望的输出应该是4,并且inc()函数里面执行操作n=n+1前后n的id值应该是不变的。可事实上并非这样;从输出来看n的值还是不变,但id(n)的值在函数体前后却不一致。显示传引用这个说法是不恰当的;
2)传值
1 >>> def change_list(orginator_list): 2 ... print("orginator list is: %s" % orginator_list) 3 ... new_list = orginator_list 4 ... new_list.append("I am new") 5 ... print("new list is: %s" % new_list) 6 ... return new_list 7 ... 8 >>> orginator_list = ["a","b","c"] 9 >>> new_list = change_list(orginator_list) 10 orginator list is: ['a', 'b', 'c'] 11 new list is: ['a', 'b', 'c', 'I am new'] 12 >>> print(new_list) 13 ['a', 'b', 'c', 'I am new'] 14 >>> print(orginator_list) 15 ['a', 'b', 'c', 'I am new'] 16 >>>
传值通俗来讲就是这个意思:你在内存中有一个位置,我也有一个位置,我把我的值复制给你,以后你做什么就跟我没关系了,我是我,你是你,咱俩井水不犯河水;可上面的程序输出根本就不是这么一回事,显示change_list()函数没有遵守约定,调用该函数之后orginator_list也发生了改变,这明显侵犯了orginator_list的权利。这样看来,传值这个说法也不合适
3)可变对象传引用,不可变对象传值
1 def change_me(org_list): 2 print(id(org_list)) 3 new_list = org_list 4 print(id(new_list)) 5 if len(new_list)>5: 6 new_list = ['a', 'b', 'c'] 7 for i, e in enumerate(new_list): 8 if isinstance(e, list): 9 new_list[i] = "***" 10 print(new_list) 11 print(id(new_list)) 12 13 14 test1 = [1, ['a', 1, 3], [2, 1], 6] 15 change_me(test1) # test1的元素个数少于5 16 print(test1) 17 18 print("=" * 20) 19 20 test2 = [1, 2, 3, 4, 5, 6, [1]] 21 change_me(test2) # test2的元素个数多于5 22 print(test2) 23 24 输出结果: 25 139812969314760 26 139812969314760 27 [1, '***', '***', 6] # test1中所有list类型的元素都替换成了*** 28 139812969314760 29 [1, '***', '***', 6] 30 ==================== 31 139812969314696 32 139812969314696 33 ['a', 'b', 'c'] 34 139812969314056 35 [1, 2, 3, 4, 5, 6, [1]] # test2并没有发生改变
传入参数org_list为列表,属于可变对象,按照可变对象传引用的理解,new_list和org_list指向同一个内存地址,因此两者的id值输出一致,任务对new_list所执行的内容的操作会直接反应到org_list,也就是说修改new_list会导致org_list的直接修改;
- 对于test1、new_list和org_list的表现确实与我们理解的传引用一致,最后test1被修改为[1, '***', '***', 6]
- 对于test2、new_list和org_list的id输出在列表相关的操作前是一致的,但操作之后,new_list的id值却变为139812969314056,整个test2在调用函数change_me后却没发生任何改变,按照传引用的理解,期望的输出结果应该是['a', 'b', 'c'],因此,似乎可变对象传引用这个说法也不恰当了。
回到开始的问题,Python函数传参的机制到底是怎样的?我们首先需要理解Python中的赋值与其他语言的不同
例如C/C++中的赋值:
a = 5, b = a, b = 7;
当执行b = a时,首先会在内存中新申请一块内存给b并将a的值复制到该内存中;
当执行b = 7之后,则是将b对应内存中的值修改为7
而Python中的赋值并不是复制,b = a操作使用b与a引用了同一个对象5,而b = 7则是将b指向了另一个对象7
我们来验证上述过程:
1 >>> a = 5 2 >>> id(a) 3 10910528 4 >>> b = a 5 >>> id(b) # b = a之后,b的id()值和a的一样 6 10910528 7 >>> b = 7 8 >>> id(b) # b = 7之后b指向对象7,id()值发生改变 9 10910592 10 >>> id(a) 11 10910528 12 >>>
从输出可以看出,b=a赋值后,b的id()输出和a的一样,但b=7操作后b指向了另外一块空间。可以简单理解为,b=a传递的是对象的引用,其过程类似于"贴标签",5和7是实实在在的在内存空间中,执行a=5相当于申请一块内存空间代表对象5并在上面贴上标签a,这样a和5就绑定在一起了。而b=a相当于对a创建了一个别名,因此他们都指向了5。但b=7操作之后标签b重新贴到了对象7上,而此时对象5只有标签a。
理解了上述赋值过程之后,就可以很好地理解前面的3个示例了;
1)对于示例1,n = n + 1,由于n为数字,是不可变对象,n+1会重新申请一块内存,并在函数体中创建局部变量n指向它。当调用完inc(n)之后,函数体中的局部变量在函数外并不可见,此时的n代表函数体外的命名空间所对应的n,值还是3
2)对于示例2,orginator_list是一个列表,是可变对象,函数体内对列表的操作直接反应到该列表对象
3)对于示例3,当org_list的长度大于5时,new_list = ["a", "b", "c"]操作重新申请了一块内存并将new_list指向它,当传入参数test2=[1, 2, 3, 4, 5, 6, [1]]时,其长度大于5,函数的执行并没有改变该列表的值
因此,对于Python函数参数是传值还是传引用这个问题的答案是:都不是,正确的叫法是"传对象"或者说"传对象的引用"。
函数参数在传递的过程中将整个对象传入,对可变对象的修改在函数内、函数外都可见,调用者和被调用者之间共享这个对象;而对于不可变对象,由于并不能真正被修改,因此往往是通过生成一个新对象然后赋值来实现的