2022年 11月 3日

python中的值传递和引用传递

 今天和大家分享python中很重要的一个知识点:参数传递,其中包括值传递和引用传递。

目录

一、为什么要熟悉值传递和引用传递

1.1 值传递

1.2 引用传递

二、Python变量及其赋值

三、Python函数的参数传递

四、总结


一、为什么要熟悉值传递和引用传递

比如,我将一个列表作为参数传入另一个函数,期望列表在函数运行结束后不变,但是往往“事与愿违”,由于某些操作,它的值改变了,那就很有可能带来后续程序一系列的错误。

因此,了解Python中参数的传递机制,具有十分重要的意义,这往往能让我们写代码时少犯错误,提高效率。今天我们就一起来学习一下,Python中参数是如何传递的。

1.1 值传递

如果你接触过其他的编程语言,比如C/C++,很容易想到,常见的参数传递有2种:值传递引用传递。所谓值传递,通常就是拷贝参数的值,然后传递给函数里的新变量。这样,原变量和新变量之间互相独立,互不影响。

下面展示一段C++代码:

  1. #include <iostream>
  2. using namespace std;
  3. // 交换2个变量的值
  4. void swap(int x, int y) {
  5. int temp;
  6. temp = x; // 交换x和y的值
  7. x = y;
  8. y = temp;
  9. return;
  10. }
  11. int main () {
  12. int a = 1;
  13. int b = 2;
  14. cout << "Before swap, value of a :" << a << endl;
  15. cout << "Before swap, value of b :" << b << endl;
  16. swap(a, b);
  17. cout << "After swap, value of a :" << a << endl;
  18. cout << "After swap, value of b :" << b << endl;
  19. return 0;
  20. }
  21. Before swap, value of a :1
  22. Before swap, value of b :2
  23. After swap, value of a :1
  24. After swap, value of b :2

这里的swap()函数,把a和b的值拷贝给了x和y,然后再交换x和y的值。这样一来,x和y的值发生了改变,但是a和b不受其影响,所以值不变。这种方式,就是我们所说的值传递。  

1.2 引用传递

所谓引用传递,通常是指把参数的引用传给新的变量,这样,原变量和新变量就会指向同一块内存地址。如果改变了其中任何一个变量的值,那么另外一个变量也会相应地随之改变。

还是拿我们刚刚讲到的C++代码为例,上述例子中的swap()函数,如果改成下面的形式,声明引用类型的参数变量:

  1. void swap(int& x, int& y) {
  2. int temp;
  3. temp = x; // 交换x和y的值
  4. x = y;
  5. y = temp;
  6. return;
  7. }

那么输出的便是另一个结果:

  1. Before swap, value of a :1
  2. Before swap, value of b :2
  3. After swap, value of a :2
  4. After swap, value of b :1

原变量a和b的值被交换了,因为引用传递使得a和x,b和y一模一样,对x和y的任何改变必然导致了a和b的相应改变。不过,这是C/C++语言中的特点。那么Python中,参数传递到底是如何进行的呢?它们到底属于值传递、引用传递,还是其他呢?在回答这个问题之前,让我们先来了解一下,Python变量和赋值的基本原理。

二、Python变量及其赋值

我们首先来看,下面的Python代码示例:

  1. a = 1
  2. b = a
  3. a = a + 1

最后的结果是,a的值变成了2,而b的值不变仍然是1。

通过这个例子你可以看到,这里的a和b,开始只是两个指向同一个对象的变量而已,或者你也可以把它们想象成同一个对象的两个名字。简单的赋值b = a,并不表示重新创建了新对象,只是让同一个对象被多个变量指向或引用。

同时,指向同一个对象,也并不意味着两个变量就被绑定到了一起。如果你给其中一个变量重新赋值,并不会影响其他变量的值。

明白了这个基本的变量赋值例子,我们再来看一个列表的例子:

  1. l1 = [1, 2, 3]
  2. l2 = l1
  3. l1.append(4)
  4. l1
  5. [1, 2, 3, 4]
  6. l2
  7. [1, 2, 3, 4]

另外,需要注意的是,Python里的变量可以被删除,但是对象无法被删除。比如下面的代码:  

  1. l = [1, 2, 3]
  2. del l

del l 删除了l这个变量,从此以后你无法访问l,但是对象[1, 2, 3]仍然存在。Python程序运行时,其自带的垃圾回收系统会跟踪每个对象的引用。如果[1, 2, 3]除了l外,还在其他地方被引用,那就不会被回收,反之则会被回收。

由此可见,在Python中:

  • 变量的赋值,只是表示让变量指向了某个对象,并不表示拷贝对象给变量;而一个对象,可以被多个变量所指向。

  • 可变对象(列表,字典,集合等等)的改变,会影响所有指向该对象的变量。

  • 对于不可变对象(字符串、整型、元组等等),所有指向该对象的变量的值总是一样的,也不会改变。但是通过某些操作(+=等等)更新不可变对象的值时,会返回一个新的对象。

  • 变量可以被删除,但是对象无法被删除。

三、Python函数的参数传递

准确地说,Python的参数传递是赋值传递 (pass by assignment),或者叫作对象的引用传递(pass by object reference)。Python里所有的数据类型都是对象,所以参数传递时,只是让新变量与原变量指向相同的对象而已,并不存在值传递或是引用传递一说。

比如,我们来看下面这个例子:

  1. def my_func1(b):
  2. b = 2
  3. a = 1
  4. my_func1(a)
  5. a
  6. 1

这里的参数传递,使变量a和b同时指向了1这个对象。但当我们执行到b = 2时,系统会重新创建一个值为2的新对象,并让b指向它;而a仍然指向1这个对象。所以,a的值不变,仍然为1。

那么对于上述例子的情况,是不是就没有办法改变a的值了呢?

答案当然是否定的,我们只需稍作改变,让函数返回新变量,赋给a。这样,a就指向了一个新的值为2的对象,a的值也因此变为2。

  1. def my_func2(b):
  2. b = 2
  3. return b
  4. a = 1
  5. a = my_func2(a)
  6. a
  7. 2

不过,当可变对象当作参数传入函数里的时候,改变可变对象的值,就会影响所有指向它的变量。比如下面的例子:

  1. def my_func3(l2):
  2. l2.append(4)
  3. l1 = [1, 2, 3]
  4. my_func3(l1)
  5. l1
  6. [1, 2, 3, 4]

这里l1和l2先是同时指向值为[1, 2, 3]的列表。不过,由于列表可变,执行append()函数,对其末尾加入新元素4时,变量l1和l2的值也都随之改变了。

但是,下面这个例子,看似都是给列表增加了一个新元素,却得到了明显不同的结果。

  1. def my_func4(l2):
  2. l2 = l2 + [4]
  3. l1 = [1, 2, 3]
  4. my_func4(l1)
  5. l1
  6. [1, 2, 3]

为什么l1仍然是[1, 2, 3],而不是[1, 2, 3, 4]呢?

要注意,这里l2 = l2 + [4],表示创建了一个“末尾加入元素4“的新列表,并让l2指向这个新的对象。这个过程与l1无关,因此l1的值不变。当然,同样的,如果要改变l1的值,我们就得让上述函数返回一个新列表,再赋予l1即可:

  1. def my_func5(l2):
  2. l2 = l2 + [4]
  3. return l2
  4. l1 = [1, 2, 3]
  5. l1 = my_func5(l1)
  6. l1
  7. [1, 2, 3, 4]

这里你尤其要记住的是,改变变量和重新赋值的区别:

  • my_func3()中单纯地改变了对象的值,因此函数返回后,所有指向该对象的变量都会被改变。

  • 但my_func4()中则创建了新的对象,并赋值给一个本地变量,因此原变量仍然不变。

至于my_func3()和my_func5()的用法,两者虽然写法不同,但实现的功能一致。不过,在实际工作应用中,我们往往倾向于类似my_func5()的写法,添加返回语句。这样更简洁明了,不易出错。

四、总结

今天,我们一起学习了Python的变量及其赋值的基本原理,并且解释了Python中参数是如何传递的。和其他语言不同的是,Python中参数的传递既不是值传递,也不是引用传递,而是赋值传递,或者是叫对象的引用传递。

需要注意的是,这里的赋值或对象的引用传递,不是指向一个具体的内存地址,而是指向一个具体的对象。

  • 如果对象是可变的,当其改变时,所有指向这个对象的变量都会改变。

  • 如果对象不可变,简单的赋值只能改变其中一个变量的值,其余变量则不受影响。

清楚了这一点,如果你想通过一个函数来改变某个变量的值,通常有两种方法。一种是直接将可变数据类型(比如列表,字典,集合)当作参数传入,直接在其上修改;第二种则是创建一个新变量,来保存修改后的值,然后将其返回给原变量。在实际工作中,我们更倾向于使用后者,因为其表达清晰明了,不易出错。