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 中:
- 变量的赋值,只是表示让变量指向了某个对象,并不表示拷贝对象给变量;而一个对象,可以被多个变量所指向或引用。
- 对于不可变对象(字符串-string,整型-int,元组-tuple等),所有指向该对象的变量的值总是一样的,也不会改变。但是通过某些操作( = 等等)更新不可变对象的值时,会返回一个新的对象。
- 对于可变对象(列表-list,字典-dict,集合-set等等)的改变,会影响所有指向该对象的变量。
- 变量可以被删除,但是对象无法被删除,对象会在无任何变量引用时被系统自动回收。
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 中赋值或对象的引用传递,不是指向一个具体的内存地址,而是指向一个具体的对象。
- 如果对象是可变的,当其改变时,所有指向这个对象的变量都会改变。
- 如果对象不可变,简单的赋值只能改变其中一个变量的值,其余变量则不受影响。