0%

Python_值传递与引用传递

参考地址:博客教程 博客详细教程 gitHub地址

相关概念

形参与实参

在函数定义中常见的函数都会选择有参数的函数形式,函数的作用是传递数据给函数,并对此数据进行具体的操作;

在使用函数时,经常会用到形式参数(形参)与实际参数(实参),二者都称为参数,其差别在于:

  • 形式参数:在定义函数时,函数名后面的参数就是形参,例如:
1
2
3
# 定义函数时采用就是形参
def demo(formalParameters):
print(formalParameters)
  • 实际参数:在调用函数时,函数的调用者给函数的参数,例如:
1
2
3
4
# 调用函数时就是实际参数
actualParameters = "形式参数还是实际参数这是个问题"
demo(actualParameters)
# 形式参数还是实际参数这是个问题

那么将实际参数传递给形式参数的过程,就可以分为值传递与引用传递啦;

值传递与引用传递

  • 相同点:都是将实参传递给形参的过程
  • 不同点:
    • 值传递:适用于不可变的参数 ,例如 字符串,元组,数字等 , 传递的是一个副本
    • 引用传递:适用于可变的参数,例如 列表,字典等,传递的是指向可变参数对象的指针
  • 结论:如何判定当前是值传递还是引用传递呢?
    • 就是看当前传递的参数本身是否可变,不可变就是值传递,可变就是引用传递。
      • 这里的可变就是能够进行修改,immutable.

其实值传递与引用传递是一回事,看接下来的描述即可;

值传递与引用传递详解

一个例子

我们先从一个例子出发看一看值传递与引用传递的对实际编程的影响;示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
def demo(obj) :
obj += obj
print("形参值为:",obj)
print("-------值传递-----")
a = "C语言中文网"
print("a的值为:",a)
demo(a)
print("实参值为:",a)
print("-----引用传递-----")
a = [1,2,3]
print("a的值为:",a)
demo(a)
print("实参值为:",a)

结果展示:

1
2
3
4
5
6
7
8
-------值传递-----
a的值为: 吃了嘛?
形参值为: 吃了嘛?吃了嘛?
实参值为: 吃了嘛?
-----引用传递-----
a的值为: [1, 2, 3]
形参值为: [1, 2, 3, 1, 2, 3]
实参值为: [1, 2, 3, 1, 2, 3]
  • 结论:

    • 在应用值传递时:函数内部对数据进行操作,并不影响函数外部的变量的值

    • 在应用引用传递时:函数内部对数据操作,影响了函数外部变量的值

      分析运行结果不难看出,在执行值传递时,改变形式参数的值,实际参数并不会发生改变;而在进行引用传递时,改变形式参数的值,实际参数也会发生同样的改变。

至于为什么会产生如此的差异也很简单,值传递传递的是值的副本,引用传递传递的是指向对象的指针的副本)。

值传递与引用传递的应用,我个人在使用时,其实一直没有很关注相关的差异,平时总是在不经意的使用,比如操作列表经常不需要返回即可,这就是引用传递的好处;在日常DEBUG中,如果对值传递与引用传递有一个清晰的认识,我想也会顺畅很多;

Python值传递机制

上面我们已经知道值传递传递的是参数的副本,对其进行操作是不会影响原参数的。接下来也是从一个简单的例子出发,简单的探讨下内存机制的情况,有利于更清晰的认识。

一个值传递的例子:

1
2
3
4
5
6
7
8
9
10
def swap(a , b) :
# 下面代码实现a、b变量的值交换
a, b = b, a
print("swap函数里,变量a的值是", \
a, ";变量b的值是", b)
a = 6
b = 9
swap(a , b)
print("交换结束后,变量a的值是", \
a , ";变量b的值是", b)
1
2
swap函数里,变量a的值是 9 ;变量b的值是 6
交换结束后,变量a的值是 6 ;变量b的值是 9

从上面的输出我们可以看出,外部的a,b变量,并不是函数内部的a,b变量;正如上面提到的,swap()函数内部的变量仅仅是主程序变量a,b的复制品罢了;下面通过示意图的方式,展示其栈分配情况:

  • 首先程序中,定义了a,b两个局部变量,两个变量在内存中存储示意图如下所示:

  • 然后执行swap()函数时,系统进入swap()函数,并将主程序中的a,b变量作为参数值传递给swap()函数,传入的是副本,并非其本身。进入swap()函数后系统已产生了4个变量,这四个变量在内存的存储示意图如下所示:

    • 可以看到,目前分别为主程序与swap()生成了两个栈区,每个栈区均分别存在a,b变量。
  • 执行swap()函数的内部交换操作,仅仅是swap()栈区的变量产生了变化;

对比图 3 与图 1,可以看到两个示意图中主程序栈区中 a、b 的值并未有任何改变,程序改变的只是 swap() 函数栈区中 a、b 的值。这就是值传递的实质:当系统开始执行函数时,系统对形参执行初始化,就是把实参变量的值赋给函数的形参变量,在函数中操作的并不是实际的实参变量

Python中的引用传递

引用传递传递的是指向对象的指针的副本),其实质其实与值传递一致,区别仅仅是是否改变了主程序中数据情况,而这并不是传递的锅;还是一个相似的例子:

1
2
3
4
5
6
7
8
9
def swap(dw):
# 下面代码实现dw的a、b两个元素的值交换
dw['a'], dw['b'] = dw['b'], dw['a']
print("swap函数里,a元素的值是",\
dw['a'], ";b元素的值是", dw['b'])
dw = {'a': 6, 'b': 9}
swap(dw)
print("交换结束后,a元素的值是",\
dw['a'], ";b元素的值是", dw['b'])

结果:

1
2
swap函数里,a元素的值是 9 ;b元素的值是 6
交换结束后,a元素的值是 9 ;b元素的值是 6

从上面的结果来看,不仅仅swap函数内部字典元素交换成功,主程序中字典元素也进行了交换。这就造成了一个错觉,再调用swap()函数时,传入的就是dw字典本身,而不是其复制品,但这是错误的,可通过下面图示进行说明:

我就存在这样的错觉;

  • 首先主程序创建了一个字典对象,并通过dw变量指向了字典对象,此时内存中包括:对象本身,指向对象的引用变量,如图:

  • 其次开始调用swap()函数,在调用时,dw变量作为参数传递给swap()函数,这里与值传递是一模一样的:将主程序中的dw变量的值赋给了swap()函数的dw形参,从而完成了swap()函数的dw参数的初始化。

    • 主程序中的dw是一个引用变量,保存了指向字典对象的地址值,当把dw值赋给swap()函数的dw参数后,函数swap()的dw参数也保存了地址值,也会应用到同一字典对象。如图:

    • 从这里可以看到这就是不折不扣的值传递,系统仍然复制了一份dw副本传入swap()函数。但是由于dw只是一个引用变量,因此系统复制的是dw变量,并没有复制对象本身。
  • 由于dw只是一个引用变量,故实际操作还是字典对象,也就看到了主程序中的dw引用的对象也发生了变化;

  • 为了更好的证明主程序中的dw和swap()函数中的dw是两个变量,在swap()最后一行添加如下代码:

1
2
#把dw 直接赋值为None,让它不再指向任何对象
dw = None

其内存如下:

从图 6 来看,把 swap() 函数中的 dw 赋值为 None 后,在 swap() 函数中失去了对字典对象的引用,不可再访问该字典对象。但主程序中的 dw 变量不受任何影响,依然可以引用该字典对象,所以依然可以输出字典对象的 a、b 元素的值。

结论:

如果需要让函数修改某些数据,则可以通过把这些数据包装成列表、字典等可变对象,然后把列表、字典等可变对象作为参数传入函数,在函数中通过列表、字典的方法修改它们,这样才能改变这些数据。