返回介绍

建议31:记住函数传参既不是传值也不是传引用

发布于 2024-01-30 22:19:09 字数 4275 浏览 0 评论 0 收藏 0

Python中的函数参数到底是传值还是传引用呢?这是许多人在学习过程中会纠结的一个问题,很多论坛也有这样的讨论。总结来说基本有3个观点:传引用;传值;可变对象传引用,不可变对象传值。这3个观点到底哪个正确呢?我们逐一讨论。

1)传引用。先来看一个非常简单的例子(请不要因为例子太简单而不以为然,小故事往往蕴含大道理,它照样能说明问题)。

示例一:

>>> def inc(n):
...  print id(n)
...  n = n+1
...  print id(n)
...
>>> n =3
>>> id(n)
34450040
>>> inc(n)
34450040  #
修改之前的n
的id
值
34450028  #
修改之后的n
的id
值
>>> print n
3
>>>

按照传引用的概念,上面的例子期望的输出应该是4,并且inc()函数里面执行操作n=n+1的前后n的id值应该是不变的。可是事实是不是这样的呢?非也,从输出结果来看n的值还是不变,但id(n)的值在函数体前后却不一致。显然,传引用这个说法是不恰当的。

2)传值。来看一个示例。

示例二:

>>> def change_list(orginator_list):
...  print "orginator list is:",orginator_list
...  new_list = orginator_list
...  new_list.append("I am new")
...  print "new list is:", new_list
...  return new_list
...
>>>
>>> orginator_list = ['a','b','c']
>>> new_list = change_list(orginator_list)
orginator list is: ['a', 'b', 'c']
new list is: ['a', 'b', 'c', 'I am new']
>>> print new_list
['a', 'b', 'c', 'I am new']
>>> print orginator_list
['a', 'b', 'c', 'I am new']
>>>

传值通俗来讲就是这个意思:你在内存中有一个位置,我也有一个位置,我把我的值复制给你,以后你做什么就跟我没关系了,我是我,你是你,咱俩井水不犯河水。可是上面的程序输出根本就不是这么一回事,显然change_list()函数没有遵守约定,调用该函数之后orginator_list也发生了改变,这明显侵犯了orginator_list的权利。这么看来传值这个说法也不合适。

3)可变对象传引用,不可变对象传值。这个说法最靠谱,很多人也是这么理解的,但这个说法到底是否准确呢?再来看一个示例。

示例三:

>>> def change_me(org_list):
...  print id(org_list)
...  new_list = org_list
...  print id(new_list)
...  if len(new_list)>5:
...       new_list = ['a','b','c']
...  for i,e in enumerate(new_list):
...       if isinstance(e,list):
...         new_list[i]="***"   #
将元素为list
类型的替换为***
...  print new_list
...  print id(new_list)
...
>>>

传入参数org_list为列表,属于可变对象,按照可变对象传引用的理解,new_list和org_list指向同一块内存,因此两者的id值输出一致,任何对new_list所执行的内容的操作会直接反应到org_list,也就是说修改new_list会导致org_list的直接修改,对吧?来看测试例子。

>>> test1 = [1,['a',1,3],[2,1],6]
>>> change_me(test1)        #test1
的元素个数小于5
35260216
35260216
[1, '***', '***', 6]
        #test1
中所有list
类型的元素都替换为了***
35260216
>>> print test1
[1, '***', '***', 6]
>>> test2=[1,2,3,4,5,6,[1]]
    #test1
中元素的个数大于5
>>> change_me(test2)
35260136
35260136
['a', 'b', 'c']
35250664 #new_list
的id
值发生了改变
>>> print test2           #test2
并没有发生改变
[1, 2, 3, 4, 5, 6, [1]]
>>>

对于test1、new_list和org_list的表现和我们理解的传引用确实一致,最后test1被修改为[1, '***', '***', 6],但对于输入test2、new_list和org_list的id输出在进行列表相关的操作前是一致的,但操作之后new_list的id值却变为35250664,整个test2在调用函数change_me()后却没有发生任何改变,可是按照传引用的理解期望输出应该是['a','b','c'],似乎可变对象传引用这个说法也不恰当。

那么Python函数中参数传递的机制到底是怎么样的?要明白这个概念,首先要理解:Python中的赋值与我们所理解的C/C++等语言中的赋值的意思并不一样。如果有如下语句:

a =5
,b = a
,b=7 ;

我们分别来看一下在C/C++以及Python中是如何赋值的。

如图3-6所示,C/C++中当执行b=a的时候,在内存中申请一块内存并将a的值复制到该内存中;当执行b=7之后是将b对应的值从5修改为7。

图3-6 C/C++赋值时内存的变化

但在Python中赋值并不是复制,b=a操作使得b与a引用同一个对象。而b=7则是将b指向对象7,如图3-7所示。

图3-7 Python中赋值语句对应的内存变化

我们通过以下示例来验证上面所述的过程:

>>> a = 5
>>> id(a)
34450016
>>> b = a
>>> id(b)        #b=a
之后b
的id()
值和a
一样
34450016
>>> b = 7
>>> id(b)        #b=7
之后b
指向对象7
,id()
值发生改变
34449992
>>> id(a)
34450016

从输出可以看出,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。

理解了上述背景,再回头来看看前面的例子就很好理解了。对于示例一,n=n+1,由于n为数字,是不可变对象,n+1会重新申请一块内存,其值为n+1,并在函数体中创建局部变量n指向它。当调用完函数inc(n)之后,函数体中的局部变量在函数外不并不可见,此时的n代表函数体外的命名空间所对应的n,值还是3。而在示例三中,当org_list的长度大于5的时候,new_list = ['a','b','c']操作重新创建了一块内存并将new_list指向它。当传入参数为test2=[1,2,3,4,5,6,[1]]的时候,函数的执行并没有改变该列表的值。

因此,对于Python函数参数是传值还是传引用这个问题的答案是:都不是。正确的叫法应该是传对象(call by object)或者说传对象的引用(call-by-object-reference)。函数参数在传递的过程中将整个对象传入,对可变对象的修改在函数外部以及内部都可见,调用者和被调用者之间共享这个对象,而对于不可变对象,由于并不能真正被修改,因此,修改往往是通过生成一个新对象然后赋值来实现的。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文