这么多年,总算搞清楚了 Python 参数是如何传递的

2023-03-06 14:00:09 浏览数 (1)

1. 常见参数传递方式

在编程语言C或C 中,常见的参数传递有 2 种:

  • 值传递
  • 引用传递

值传递,通常就是拷贝参数的值,然后传递给函数里的新变量。这样,原变量和新变量之间互相独立,互不影响。

引用传递,通常是指把参数的引用传给新的变量,这样,原变量和新变量就会指向同一块内存地址。

如果改变了其中任何一个变量的值,那么另外一个变量也会相应地随之改变。

了解值传递与引用传递后,大家思考下,Python 中参数传递是值传递,还是引用传递,或是其他方式呢?

在回答这个问题前,先来了解 Python 中变量与赋值原理。

2. Python变量与赋值原理
2.1 不可变数据类型

先来看下下面这段 Python 变量与赋值的代码:

代码语言:javascript复制
1 tony_age = 18
2 tom_age = tony_age
3 tony_age = tony_age   12

上面三行代码的变量与赋值过程,绘制成下图。

第1行代码:将 18 赋值于 tony_age,即 tony_age 这个变量指向了 18 这个对象;

第2行代码:tom_age = tony_age 则表示,让变量 tom_age 也同时指向 18 这个对象;

PS: Python 里的对象可以被多个变量所指向或引用。

第3行代码:最后执行tony_age = tony_age 12;

PS: Python 的数据类型中整型(int)、字符串(string)等是不可变的。

所以,tony_age = tony_age 12,并不是让 tony_age 的值增加 12,而是表示重新创建了一个新的值为 30 的对象,并让 tony_age 这个变量指向它。但是 tom_age 仍保持不变,仍然指向 18 这个对象。

因此,从上图可见,当执行完第3行代码后的结果是:

tony_age 的值变成了 30,而 tom_age 的值不变仍然是 18。

在 Python 中,这里的 tony_age 与 tom_age 刚开始只是两个指向同一个对象的变量而已,或者你也可以把这两个变量想象成同一个对象的两个名字。

简单的 tom_age = tony_age 赋值,并不表示重新创建了新对象,只是让同一个对象被多个变量指向或引用。

2.2 可变数据类型

2.1是数据类型为整型(int)的赋值举例说明,在 Python 中整型为不可变数据类型

下面将使用 Python 的可变数据类型列表(list)来举例,示例代码如下:

Input:

代码语言:javascript复制
1 list1 = [1, 3, 5]
2 list2 = list1
3 list1.append(7)

Output:

代码语言:javascript复制
print(list1)
[1, 3, 5, 7]

print(list2)
[1, 3, 5, 7]

上面三行代码的变量与赋值过程,绘制成下图。

第1行代码:将列表 [1, 3, 5] 赋值于 list1,即 list1 这个变量指向了 [1, 3, 5] 这个对象;

第2行代码:list2 = list1 则表示,让变量 list2 也同时指向 [1, 3, 5] 这个对象;

第3行代码:最后执行list1.append(7),由于列表是可变的,所以 list1.append(7) 不会创建新的列表,只是在原列表的末尾插入了元素 7,变成 [1, 3, 5, 7]

因为 list1 和 list2 同时指向这个列表,所以列表的变化会同时反映在 list1 和 list2 这两个变量上,因此 list1 和 list2 的值就同时变为了[1, 3, 5, 7]

PS: Python 里的变量可以被删除,但是对象无法被删除。

示例代码:

代码语言:javascript复制
list1 = [1, 3, 5, 7]
del list1

del list1 删除了 list1 这个变量,因此无法再访问到 list1,但是其引用的对象 [1, 3, 5, 7] 仍然存在。

Python 程序运行时有其自带的垃圾回收系统会跟踪每个对象的引用。如果[1, 3, 5, 7] 除了 list1 外,还在其他地方被引用,那就不会被回收,反之当唯一引用该对象的变量 list1 被删除后则会被自动回收。

通过上面的示例讲解,总结知识点如下,在 Python 中:

  1. 变量的赋值,只是表示让变量指向了某个对象,并不表示拷贝对象给变量;而一个对象,可以被多个变量所指向或引用。
  2. 对于不可变对象(字符串-string,整型-int,元组-tuple等),所有指向该对象的变量的值总是一样的,也不会改变。但是通过某些操作( = 等等)更新不可变对象的值时,会返回一个新的对象。
  3. 对于可变对象(列表-list,字典-dict,集合-set等等)的改变,会影响所有指向该对象的变量。
  4. 变量可以被删除,但是对象无法被删除,对象会在无任何变量引用时被系统自动回收。
3. Python 函数的参数传递

Python 的参数传递是赋值传递,或者叫作对象的引用传递。

Python 里所有的数据类型都是对象,所以参数传递时,只是让新变量与原变量指向相同的对象而已。

3.1 不可变数据类型的参数传递

示例代码:不改变原变量值

代码语言:javascript复制
1 def test_func1(age2):
2     age2 = 20
3 
4 age1 = 10
5 test_func1(age1)
6 print(age1)
7 10

这里的参数传递,使变量 age1 和 age2 同时指向了 10 这个对象。

但当执行到第2行 age2 = 20 时,系统会重新创建一个值为 20 的新对象,并让 age2 指向它

而 age1 仍然指向 10 这个对象。所以 age1 的值不变,仍然为 10。

如果想要通过参数传递在函数 test_func1 内部逻辑来修改 age1 的值呢?可以通过下面的代码实现。

示例代码:改变原变量值

代码语言:javascript复制
1 def test_func2(age2):
2    age2 = 20
3    return age2
4 
5 age1 = 10
6 age1 = test_func2(age1)
7 print(age1)
8 20

通过让函数返回新变量,并赋给 age1。这样 age1 就指向了一个新的值为 20 的对象,age1 的值也因此变为 20,而不再是 10。

3.2 可变数据类型的参数传递

示例代码:改变原变量值

代码语言:javascript复制
1 def test_func3(list2):
2   list2.append(7)
3 
4 list1 = [1, 3, 5]
5 test_func3(list1)
6 print(list1)
7 [1, 3, 5, 7]

从上面示例代码中print(list1)的结果来看,当可变对象 list1 作为参数传入函数 test_func3 里的时候,改变可变对象的值(list2.append(7)---改变了可变对象list1的值),就会影响所有指向它的变量,因此 list1 的输出结果为[1, 3, 5, 7]而非[1, 3, 5]

示例代码:不改变原变量值

代码语言:javascript复制
1 def test_func4(list2):
2   list2 = list2   [7]
3 
4 list1 = [1, 3, 5]
5 test_func4(list1)
6 print(list1)
7 [1, 3, 5]

从上面示例代码中print(list1)的结果来看,当可变对象 list1 作为参数传入函数 test_func4 里的时候

list2 = list2 [7]表示创建了一个“末尾加入元素 7”的新列表,并让 list2 指向这个新的对象。这个过程与 list1 无关,因此 list1 的值不变,仍为[1, 3, 5]

如果想要在函数 test_func4 中改变 list1 的值,可以将 list2 指向的这个新对象 return,再调用 test_func4 函数时重新赋值给 list1,演变后的代码如下:

代码语言:javascript复制
1 def test_func5(list2):
2    list2 = list2   [7]
3    return list2
4
5 list1 = [1, 3, 5]
6 list1 = test_func5(list1)
7 print(list1)
8 [1, 3, 5, 7]

函数 test_func3() 和 test_func5() 的用法,两者虽然写法不同,但实现的功能一致。

在实际工作应用中,更推荐 test_func5() 的写法,添加返回语句。这样比较简洁明了,不容易出错。

小结

Python 中赋值或对象的引用传递,不是指向一个具体的内存地址,而是指向一个具体的对象。

  • 如果对象是可变的,当其改变时,所有指向这个对象的变量都会改变。
  • 如果对象不可变,简单的赋值只能改变其中一个变量的值,其余变量则不受影响。

0 人点赞