python 传值 传引用 可变对象 不可变对象 的区别与联系

Anbinx 2021-05-12 PM 110℃ 0条

引子:先学的c和c++后学的python,一直以来感觉很模糊或者说搞不懂的点就是python里面函数参数的传递方式。对象传入函数,什么情况下是“传值”什么时候是“传引用”,这关系到函数内部对对象作出修改时是否会引起对象在函数作用域外同样发生改变的问题,很多的python入门教程都没有讲清楚,网上搜索了很久,终于找到了一篇阐述的完整清晰的文章,收录在此,以下是正文:


可变对象 与 不可变对象

我们知道在python中一切皆对象。在python世界中可以把对象大体分成两大类:

  • 不可变对象:数字(int,float, double)、字符串、元组(tuple)、function等
  • 可变对象:字典(dict)、列表(list)、集合(set)、程序自定义的对象

所谓不可变对象就是对象的内容不能改变。给人感觉有点像c++的const修饰的对象。但是我们可以对一个不可变对象进行赋值(写操作),这时候这个不可变的对象就指向了一个新的地址空间,指向的内容也更新成新赋值的内容

可变对象就是对对象本身的修改(写操作)是直接作用于对象自身的,不会产生新的对象

通过内置函数id()来获取变量的地址,下面举例说明:

num = 1
print "num addr:",id(num)
num += 2
print "num addr:",id(num)
 
num addr: 74024648
num addr: 74024624

执行num += 2给不可变对象num赋值的时候会产生一个新的地址空间,并用新的内容填充这块地址空间,正如上面的输出所示,此时的num的地址已经改变

下面再看一个

listnum = [1,23,2]
print "list addr:",id(listnum)
listnum.append(11)
print "list addr:",id(listnum)
print listnum
 
list addr: 79300608
list addr: 79300608
[1, 23, 2, 11]

list是可变对象,修改一个可换对象可直接作用于这个可变对象,所以最终的list就插入了一个元素

传值还是传引用

有了前面的可变对象与不可变对象的认识,我们在就很容易理解传值还是传引用的问题。但是先说个结论:python中不存在所谓的传值的函数调用,一切都是传递引用

我们知道函数传递的参数是进行从实参到形参的赋值操作进行的。在python中我们知道,不管是可变对象还是不可变对象,所有的赋值操作并没有产生实质性的东西,新赋值的对象和源对象的地址始终是一样的,也就是说两者在赋值操作完成后指向的是同一块内存。我们可以看下面的例子:

num = 1
num_bk = num
print "num addr:%d   num_bk addr:%d" %(id(num),id(num_bk))
 
listnum = [1,23,2]
listnum_bk = listnum
print "listnum addr:%d   listnum_bk addr:%d"%(id(listnum),id(listnum_bk))
 
 
num addr:77104840   num_bk addr:77104840
listnum addr:79104000   listnum_bk addr:79104000

可以看出不管是可变对象还是不可变对象,赋值后两个对象的地址是相同的。也证实了上面的结论。所以问题的关键就变成了我们对新赋值的对象进行修改是否会影响到原对象的值。这个问题似乎回到了上面的对可变对象和不可变对象的写操作的问题。

通过上面的讲解我们知道:

  • 对可变对象进行写操作是直接作用于写对象本身。
  • 对不可变对象进行写操作会产生一个新的地址空间,并用新的内容填充这块地址空间

到此我们应该很容易知道:

  • 当我们传递一个不可变对象的参数到函数中(相当于通过赋值给函数的形参),如果在函数中对这个对象进行修改(写操作)是不会改变原始对象的值,因为会产生一个新的对象,这样的调用给人感觉是传值的,但其实原理不一样。
  • 当我们传递一个可变对象到函数中,如果函数中对这个对象进行修改(写操作),这个是会影响到原来的对象的 看下面具体的例子:
def UpdateNum(num):
    print "begin update num addr:",id(num)
    num += 2
    print "after uodate num addr:",id(num)
 
num = 1
print "begin call function num add:",id(num)
UpdateNum(num)
print "after call function num add:",id(num)
 
 
begin call function num add: 77104840
begin update num addr: 77104840
after uodate num addr: 77104816
after call function num add: 77104840

如何实现可变对象的传值和不可变对象的传引用

这个问题这样表述可能有点问题,但是意思可能都明白。就是想实现,传入的是可变对象时,函数内部的修改不会影响到原来的对象;传入是不可变对象,函数内部的修改能够影响到原来的对象

对于后者的解决方案比较简单,就是函数通过返回值回传这个新的对象,调用方用原来的对象来接收这个新的对象就达到了这个效果。但是要实现可变对象的传值问题就要借助于复制的内置函数了。这个介绍两个拷贝函数:copy(浅拷贝)、deepcopy(深拷贝)

浅拷贝

浅拷贝不会拷贝子对象,只是拷贝了子对象的引用,下面举例说明:

listnum = [1,2,[11,22],3]
listcopy = copy.copy(listnum)
 
listcopy.append(4)
print "list value :", listnum
print "listcopy value:", listcopy
 
listnum[2].append(33)
print "list value :", listnum
print "listcopy value:", listcopy
 
 
list value : [1, 2, [11, 22], 3]
listcopy value: [1, 2, [11, 22], 3, 4]
list value : [1, 2, [11, 22, 33], 3]
listcopy value: [1, 2, [11, 22, 33], 3, 4]

可以看到我们子对象listnum[2]的修改会作用到复制的两个对象,而深拷贝就是解决这个问题的

深拷贝

相对于浅拷贝而言,深拷贝会连子对象的内存也会拷贝一份,对子对象的修改不会影响到源对象,还是刚才的例子

listnum = [1,2,[11,22],3]
listcopy = copy.deepcopy(listnum)
 
listcopy.append(4)
print "list value :", listnum
print "listcopy value:", listcopy
 
listnum[2].append(33)
print "list value :", listnum
print "listcopy value:", listcopy
 
 
list value : [1, 2, [11, 22], 3]
listcopy value: [1, 2, [11, 22], 3, 4]
list value : [1, 2, [11, 22, 33], 3]
listcopy value: [1, 2, [11, 22], 3, 4]

可以看到所有的修改都是独立的

熟悉c++的同学可能对深浅拷贝理解起来更容易。深浅拷贝的区别就是对指针成员对象进行的是仅仅指针的复制还是对指针所指示的内存空间进行复制。仅复制指针的话,由于两个指针同时指向同一块内存,所以修改是同步的

标签: 转载, python

非特殊说明,本博所有文章均为博主原创。

评论呢


captcha
请输入验证码