blackeyes1023

********************************************************************

*****摘自<编写高质量代码 改善Python程序的91个建议>*****

********************************************************************

  Python中的函数参数到底是传值还是传引用?这个许多人在学习过程中会纠结的一个问题,总结有如下3点:

  1. 传引用
  2. 传值
  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函数参数是传值还是传引用这个问题的答案是:都不是,正确的叫法是"传对象"或者说"传对象的引用"。

  函数参数在传递的过程中将整个对象传入,对可变对象的修改在函数内、函数外都可见,调用者和被调用者之间共享这个对象;而对于不可变对象,由于并不能真正被修改,因此往往是通过生成一个新对象然后赋值来实现的

相关文章: