极客时间已完结课程限时免费阅读

16 | 值传递,引用传递or其他,Python里参数是如何传递的?

16 | 值传递,引用传递or其他,Python里参数是如何传递的?-极客时间

16 | 值传递,引用传递or其他,Python里参数是如何传递的?

讲述:冯永吉

时长10:43大小9.78M

你好,我是景霄。
在前面的第一大章节中,我们一起学习了 Python 的函数基础及其应用。我们大致明白了,所谓的传参,就是把一些参数从一个函数传递到另一个函数,从而使其执行相应的任务。但是你有没有想过,参数传递的底层是如何工作的,原理又是怎样的呢?
实际工作中,很多人会遇到这样的场景:写完了代码,一测试,发现结果和自己期望的不一样,于是开始一层层地 debug。花了很多时间,可到最后才发现,是传参过程中数据结构的改变,导致了程序的“出错”。
比如,我将一个列表作为参数传入另一个函数,期望列表在函数运行结束后不变,但是往往“事与愿违”,由于某些操作,它的值改变了,那就很有可能带来后续程序一系列的错误。
因此,了解 Python 中参数的传递机制,具有十分重要的意义,这往往能让我们写代码时少犯错误,提高效率。今天我们就一起来学习一下,Python 中参数是如何传递的。

什么是值传递和引用传递

如果你接触过其他的编程语言,比如 C/C++,很容易想到,常见的参数传递有 2 种:值传递引用传递。所谓值传递,通常就是拷贝参数的值,然后传递给函数里的新变量。这样,原变量和新变量之间互相独立,互不影响。
比如,我们来看下面的一段 C++ 代码:
#include <iostream>
using namespace std;
// 交换2个变量的值
void swap(int x, int y) {
int temp;
temp = x; // 交换x和y的值
x = y;
y = temp;
return;
}
int main () {
int a = 1;
int b = 2;
cout << "Before swap, value of a :" << a << endl;
cout << "Before swap, value of b :" << b << endl;
swap(a, b);
cout << "After swap, value of a :" << a << endl;
cout << "After swap, value of b :" << b << endl;
return 0;
}
Before swap, value of a :1
Before swap, value of b :2
After swap, value of a :1
After swap, value of b :2
这里的 swap() 函数,把 a 和 b 的值拷贝给了 x 和 y,然后再交换 x 和 y 的值。这样一来,x 和 y 的值发生了改变,但是 a 和 b 不受其影响,所以值不变。这种方式,就是我们所说的值传递。
所谓引用传递,通常是指把参数的引用传给新的变量,这样,原变量和新变量就会指向同一块内存地址。如果改变了其中任何一个变量的值,那么另外一个变量也会相应地随之改变。
还是拿我们刚刚讲到的 C++ 代码为例,上述例子中的 swap() 函数,如果改成下面的形式,声明引用类型的参数变量:
void swap(int& x, int& y) {
int temp;
temp = x; // 交换x和y的值
x = y;
y = temp;
return;
}
那么输出的便是另一个结果:
Before swap, value of a :1
Before swap, value of b :2
After swap, value of a :2
After swap, value of b :1
原变量 a 和 b 的值被交换了,因为引用传递使得 a 和 x,b 和 y 一模一样,对 x 和 y 的任何改变必然导致了 a 和 b 的相应改变。
不过,这是 C/C++ 语言中的特点。那么 Python 中,参数传递到底是如何进行的呢?它们到底属于值传递、引用传递,还是其他呢?
在回答这个问题之前,让我们先来了解一下,Python 变量和赋值的基本原理。

Python 变量及其赋值

我们首先来看,下面的 Python 代码示例:
a = 1
b = a
a = a + 1
这里首先将 1 赋值于 a,即 a 指向了 1 这个对象,如下面的流程图所示:
接着 b = a 则表示,让变量 b 也同时指向 1 这个对象。这里要注意,Python 里的对象可以被多个变量所指向或引用。
最后执行 a = a + 1。需要注意的是,Python 的数据类型,例如整型(int)、字符串(string)等等,是不可变的。所以,a = a + 1,并不是让 a 的值增加 1,而是表示重新创建了一个新的值为 2 的对象,并让 a 指向它。但是 b 仍然不变,仍然指向 1 这个对象。
因此,最后的结果是,a 的值变成了 2,而 b 的值不变仍然是 1。
通过这个例子你可以看到,这里的 a 和 b,开始只是两个指向同一个对象的变量而已,或者你也可以把它们想象成同一个对象的两个名字。简单的赋值 b = a,并不表示重新创建了新对象,只是让同一个对象被多个变量指向或引用。
同时,指向同一个对象,也并不意味着两个变量就被绑定到了一起。如果你给其中一个变量重新赋值,并不会影响其他变量的值。
明白了这个基本的变量赋值例子,我们再来看一个列表的例子:
l1 = [1, 2, 3]
l2 = l1
l1.append(4)
l1
[1, 2, 3, 4]
l2
[1, 2, 3, 4]
同样的,我们首先让列表 l1 和 l2 同时指向了[1, 2, 3]这个对象。
由于列表是可变的,所以 l1.append(4) 不会创建新的列表,只是在原列表的末尾插入了元素 4,变成[1, 2, 3, 4]。由于 l1 和 l2 同时指向这个列表,所以列表的变化会同时反映在 l1 和 l2 这两个变量上,那么,l1 和 l2 的值就同时变为了[1, 2, 3, 4]。
另外,需要注意的是,Python 里的变量可以被删除,但是对象无法被删除。比如下面的代码:
l = [1, 2, 3]
del l
del l 删除了 l 这个变量,从此以后你无法访问 l,但是对象[1, 2, 3]仍然存在。Python 程序运行时,其自带的垃圾回收系统会跟踪每个对象的引用。如果[1, 2, 3]除了 l 外,还在其他地方被引用,那就不会被回收,反之则会被回收。
由此可见,在 Python 中:
变量的赋值,只是表示让变量指向了某个对象,并不表示拷贝对象给变量;而一个对象,可以被多个变量所指向。
可变对象(列表,字典,集合等等)的改变,会影响所有指向该对象的变量。
对于不可变对象(字符串、整型、元组等等),所有指向该对象的变量的值总是一样的,也不会改变。但是通过某些操作(+= 等等)更新不可变对象的值时,会返回一个新的对象。
变量可以被删除,但是对象无法被删除。

Python 函数的参数传递

从上述 Python 变量的命名与赋值的原理讲解中,相信你能举一反三,大概猜出 Python 函数中参数是如何传递了吧?
这里首先引用 Python 官方文档中的一段说明:
“Remember that arguments are passed by assignment in Python. Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per Se.”
准确地说,Python 的参数传递是赋值传递 (pass by assignment),或者叫作对象的引用传递(pass by object reference)。Python 里所有的数据类型都是对象,所以参数传递时,只是让新变量与原变量指向相同的对象而已,并不存在值传递或是引用传递一说。
比如,我们来看下面这个例子:
def my_func1(b):
b = 2
a = 1
my_func1(a)
a
1
这里的参数传递,使变量 a 和 b 同时指向了 1 这个对象。但当我们执行到 b = 2 时,系统会重新创建一个值为 2 的新对象,并让 b 指向它;而 a 仍然指向 1 这个对象。所以,a 的值不变,仍然为 1。
那么对于上述例子的情况,是不是就没有办法改变 a 的值了呢?
答案当然是否定的,我们只需稍作改变,让函数返回新变量,赋给 a。这样,a 就指向了一个新的值为 2 的对象,a 的值也因此变为 2。
def my_func2(b):
b = 2
return b
a = 1
a = my_func2(a)
a
2
不过,当可变对象当作参数传入函数里的时候,改变可变对象的值,就会影响所有指向它的变量。比如下面的例子:
def my_func3(l2):
l2.append(4)
l1 = [1, 2, 3]
my_func3(l1)
l1
[1, 2, 3, 4]
这里 l1 和 l2 先是同时指向值为[1, 2, 3]的列表。不过,由于列表可变,执行 append() 函数,对其末尾加入新元素 4 时,变量 l1 和 l2 的值也都随之改变了。
但是,下面这个例子,看似都是给列表增加了一个新元素,却得到了明显不同的结果。
def my_func4(l2):
l2 = l2 + [4]
l1 = [1, 2, 3]
my_func4(l1)
l1
[1, 2, 3]
为什么 l1 仍然是[1, 2, 3],而不是[1, 2, 3, 4]呢?
要注意,这里 l2 = l2 + [4],表示创建了一个“末尾加入元素 4“的新列表,并让 l2 指向这个新的对象。这个过程与 l1 无关,因此 l1 的值不变。当然,同样的,如果要改变 l1 的值,我们就得让上述函数返回一个新列表,再赋予 l1 即可:
def my_func5(l2):
l2 = l2 + [4]
return l2
l1 = [1, 2, 3]
l1 = my_func5(l1)
l1
[1, 2, 3, 4]
这里你尤其要记住的是,改变变量和重新赋值的区别:
my_func3() 中单纯地改变了对象的值,因此函数返回后,所有指向该对象的变量都会被改变;
但 my_func4() 中则创建了新的对象,并赋值给一个本地变量,因此原变量仍然不变。
至于 my_func3() 和 my_func5() 的用法,两者虽然写法不同,但实现的功能一致。不过,在实际工作应用中,我们往往倾向于类似 my_func5() 的写法,添加返回语句。这样更简洁明了,不易出错。

总结

今天,我们一起学习了 Python 的变量及其赋值的基本原理,并且解释了 Python 中参数是如何传递的。和其他语言不同的是,Python 中参数的传递既不是值传递,也不是引用传递,而是赋值传递,或者是叫对象的引用传递。
需要注意的是,这里的赋值或对象的引用传递,不是指向一个具体的内存地址,而是指向一个具体的对象。
如果对象是可变的,当其改变时,所有指向这个对象的变量都会改变。
如果对象不可变,简单的赋值只能改变其中一个变量的值,其余变量则不受影响。
清楚了这一点,如果你想通过一个函数来改变某个变量的值,通常有两种方法。一种是直接将可变数据类型(比如列表,字典,集合)当作参数传入,直接在其上修改;第二种则是创建一个新变量,来保存修改后的值,然后将其返回给原变量。在实际工作中,我们更倾向于使用后者,因为其表达清晰明了,不易出错。

思考题

最后,我为你留下了两道思考题。
第一个问题,下面的代码中, l1、l2 和 l3 都指向同一个对象吗?
l1 = [1, 2, 3]
l2 = [1, 2, 3]
l3 = l2
第二个问题,下面的代码中,打印 d 最后的输出是什么呢?
def func(d):
d['a'] = 10
d['b'] = 20
d = {'a': 1, 'b': 2}
func(d)
print(d)
欢迎留言和我分享,也欢迎你把这篇文章分享给你的同事、朋友,一起在交流中进步。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 27

提建议

上一篇
15 | Python对象的比较、拷贝
下一篇
17 | 强大的装饰器
unpreview
 写留言

精选留言(82)

  • Jingxiao
    置顶
    2019-06-17
    关于思考题: 第一题: l2和l3是指向同一个对象,因为两者之间用等号赋值了,l1并不是,l1所指向的[1, 2, 3]是另外一块内存空间,大家可以通过id()这个函数验证 第二题: 输出的是{'a': 10, 'b': 20},字典是可变的,传入函数后,函数里的d和外部的d实际上都指向同一个对象 d[idx] = value语句改变了字典对应key所指向的值
    展开
    共 3 条评论
    89
  • 不瘦到140不改名
    2019-06-14
    针对@小恶魔的问题,回复一下 python里面一切皆对象, 比如a=1。在java里面是int a = 1,相当于先声明了一个int类型的变量a,然后给这个变量赋值为1。但在python中,是先在内存中申请一份空间,存的值为1,然后再给这块空间贴上一个标签,叫变量a,因此python中变量实际上是一个便利贴,可以贴在任何地方。并且还可以通过值来推断出变量的类型,这一步是由解释器来完成的。所以python虽然不需要显式声明变量,但它其实是强类型语言。 def func(d): d['a'] = 10 d['b'] = 20 d = {'a': 1, 'b': 2} d = {} func(d) print(d) # {'a': 10, 'b': 20} 至于这里为什么会是这个结果,当我们将d传递给func的时候,其实func里面的d和外面的d指向的是同一片内存。相当于一开始d={},存放{}这份空间只有d这一个便利贴,但是func(d)的时候,这份空间又多了一个便利贴。尽管都叫d,但一个是全局变量d,一个是函数的参数d 当d['a'] = 10和d['b']=20的时候,由于字典是可变类型,所以外面的d也被修改了,此时外面的d和函数里面的d都指向了{'a': 10, 'b': 20}, 但是当d = {'a': 1, 'b': 2}的时候,这是属于赋值。因此python会在内存中再开辟一份空间,空间存放{'a': 1, 'b': 2},然后让函数里面的局部变量d指向它,相当于将原本位于{'a':10,'b':20}上的便利贴撕下来,贴在了另一块空间。但这只是函数里面的d,对外面的d是没有影响的,所以外面的d依旧是{'a': 10, 'b': 20}。
    展开
    共 8 条评论
    84
  • somenzz
    2019-06-14
    第一个比较简单,列表是可变对象,每创建一个列表,都会重新分配内存,因此 l1 和 l2 并不是同一个对象,由于 l3 = l2 表明 l3 指向 l2 的对象。 第二个 输出的结果应该是 {'a': 10, 'b': 20} ,d = {'a': 1, 'b': 2} 属于重新指向新的对象,并不改变原有的字典对象。
    23
  • yshan
    2019-06-16
    首先更正下,需要先定义d={}。 然后,局部变量与全局变量的区别,函数内定义的d为全局变量,在没有关键字声明的情形下不能改变全局变量,由于字典可变,遵循可变则可变的原则,输出为{'a': 10, 'b': 20}。 最后,看实验: def func(d): print(id(d)) d['a'] = 10 d['b'] = 20 print(id(d)) d = {'a':1, 'b':2} print(id(d)) print(d) d = {} print(id(d)) func(d) print(d) print(id(d)) 执行结果: 3072243980 3072243980 3072243980 3072244108 {'a': 1, 'b': 2} {'a': 10, 'b': 20} 3072243980
    展开
    共 2 条评论
    10
  • 程序员人生
    2019-06-14
    第一题,用id()打印出来后可以证明,l1和l2不是同一个对象,l2和l3是同一个对象。由于列表是可变的,所以l1和l2指向不同的内存区域。 第二题,做了一下修改,如下: def func(d): d['a'] = 10 d['b'] = 20 d={'a':1,'b':2} d={} func(d) print(d) 执行结果: {'a': 10, 'b': 20} d = {'a': 1, 'b': 2}应该是指向了新的对象
    展开
    共 1 条评论
    8
  • Wing·三金
    2019-06-16
    # C++ - 按值传递:拷贝参数的值构建新的变量传递到函数 - 按引用传递:把参数的引用(i.e. 地址)传递到函数 # Python - 按赋值传递/按对象的引用传递 - 凡是对对象本身进行的操作,都会影响传递的原对象;凡是生成了新对象的操作,都不会影响传递的原对象 - 正如【一个人可以死两次,第一次是肉体死去,第二次是当没人记得它的时候】,python 中如果有多个变量指向同一个对象,那么当删除一个变量时并不会真正删除其所指定的对象;只有当所有指定该对象的变量都被删除时,python 才会回收该对象所占用的资源 - 一般原则:对于不可变的数据类型,operator 等操作会返回新的对象,不会影响原对象;对于可变的数据类型,任何对【对象本身】的操作都会影响所有指向该对象的变量 - 补充上一条:e.g. 对于 list 而言,l += [1] 和 l = l + [1] 不同!前者是在 l 本身的末尾添加新元素,后者是在 l 的基础上添加新的元素并返回新的对象 - 在工程上,偏爱类似于上一条后者的作法——即通过【创建新的对象+将其返回】的作法,来减少出错的概率 # 思考题 1. l1 与 l2 不同,l3 与 l2 同; 2. 严格来说,如果没有上下文,这是一段错误的代码,因为没有预先定义 d 变量;不妨假设在第 6 行之前补充语句 d = {},则输出结果为 {'a': 10, 'b': 20},因为 func 中前两行才是改变了对象的操作。而第 3 行只是将函数中的局部变量 d 指向了新的字典 {'a': 1, 'b': 2},但全局变量 d 仍然指向着刚刚被修改过的字典对象。
    展开
    7
  • SCAR
    2019-06-14
    第一题:l2和l3指向同一个对象,l2和l1不指向同一个对象。这个题的关键要点是要了解list对象是没有“内存驻留”机制的,这点和整数对象对小于256的数采用的“内存驻留”是截然不同的,所以l1和l2不是指向同一对象。而l3=l2,这就是让l3指向l2指向的对象,很显然l3和l2指向的是同一个对象。 第二题:题目里的d = {'a': 1, 'b': 2}应该是顶格的吧,估计是老师手误或是编辑器出问题了,不然没意义。如果是这样,print(d),输出应该是{'a': 10, 'b': 20}。
    展开
    5
  • 自由民
    2019-10-03
    总结:Python中参数传递既不是传值也不是传引用,而是赋值传递,或传对象的引用。不是指向一个具体的内存地址,而是指向具体的对象。 如果对象是不变的,改变对象会新建一个对象,并将其中一个变量指向该对象,其它变量不变。如果对象是可变的,改变一个变量时,其它所有指向该对象的变量都会受影响。要想在函数中改变对象,可以传入可变数据类型(列表,字典,集合),直接改变;也可以创建一个新对象,修改以后返回。建议用后者,表达清晰明了,不易出错。 思考题1: l2与l3指向同一对象,与l1不同。 # 思考题1 l1 = [1,2,3,4] l2 = [1,2,3,4] l3 = l2 print(id(l1), id(l2), id(l3)) 思考题2 {"a":10, "b":20} 课程的练习代码: https://github.com/zwdnet/PythonPractice
    展开
    3
  • youaresherlock
    2020-06-29
    Python应该是共享传参
    2
  • 1cho糖糖
    2019-11-03
    遇到了下面一个问题 ``` def demo1(array): array += [4, 5] return array def demo2(array): array = array + [4, 5] return array a = [1, 2, 3] b = [1, 2, 3] c = demo1(a) print('a list is {}\nc list is {}\na is c :{}'.format(a, c, a is c)) # True # 输出结果 a list is [1, 2, 3, 4, 5] c list is [1, 2, 3, 4, 5] a is c :True d = demo2(b) print('b list is {}\nd list is {}\nb is d :{}'.format(b, d, b is d)) # False # 输出结果 b list is [1, 2, 3] d list is [1, 2, 3, 4, 5] b is d :False # 函数内部为什么 array += [4, 5] 与 array = array + [4, 5] 对传入的列表影响结果不同 ```
    展开
    共 1 条评论
    2
  • catshitfive
    2019-06-14
    第一题:本例中,对于列表对象而言,l1和l2是不同的对象,l3指向的是l2,属于同一对象;对于列表内的immutable元素对象而言,这三个列表指向的都是相同的对象 第二题 使用数据结构中内置的方法或者切片操作会直接修改可变元素的内容而保持内存地址不变,如果是二个对象直接操作,则会创建新对象,所以本例中变量指向的字典内容会被函数更新 对于一维列表可以浅拷贝保存数据,对于二维及以上的,应该用深拷贝保存数据才安全
    展开
    2
  • KaitoShy
    2019-06-14
    1. l2和l3是一个,l1不是。可以通过id(l2),id(l1),id(l3)验证。 2. d不是没有初始化么。输出错误吧。如果在使用函数func(),将d初始化为d={},输出{'a':10, 'b':20}.原因:前两个改变了对象的值。后面是创建了新对象赋值给了本地对象。
    2
  • Jacky
    2021-01-25
    不可变对象,赋值给变量时,如果两个对象的值相同,则指向同一个对象。 两个变量指向同一对象,如果其中一个对象的值改变,则新建对象赋值给变量。 可变对象,赋值给变量时,如果两个对象的值相同,则指向不同对象。 两个变量指向同一对象,如果其中一个对象的值改变,两个变量的值都发生改变。(指向统一对象)
    1
  • 轻风悠扬
    2020-05-10
    老师,最后一个代码示例,如果我把l2 = l2 + [4] 换成 l2 += [4],l1会变成[1,2,3,4].不明白l2 = l2 + [4] 换成 l2 += [4]有什么不一样的地方,有点小困惑 def my_func5(l2): l2 += [4] l1 = [1, 2, 3] my_func5(l1) l1 [1, 2, 3, 4]
    展开

    作者回复: l2 = l2 + [4]和l2 += [4]是一样的啊,程序运行的结果都是[1, 2, 3, 4]

    共 7 条评论
    1
  • 张丽娜
    2019-06-14
    def my_func2(b): print('a的值是{}'.format(a)) print('b的值是{}'.format(a)) b = 2 print('b的值是{}'.format(b)) return b a = 1 a = my_func2(a) #这句话so 重要,重新用返回值对于a进行了赋值,看起来debug来逐步分析很重要啊 print('a的值是{}'.format(a))
    展开
    1
  • 小恶魔
    2019-06-14
    看了这么多关于问题2回复都是结果。我想知道python中对参数赋值不会影响外部的值,这是设定语法,还有什么深层次的原因或设计考虑么,谢谢老师。
    共 1 条评论
    1
  • 这只鸟不会飞
    2023-01-18 来自贵州
    关于思考题: 第一题:l1 和 l2 不指向同一个对象,l2 和 l3 指向同一个对象。 从题干来看,l1和l2分别是创建了新对象来指向地址的,使用 l1 is l2 就能分辨出来。而l2 和 l3 指向的是同一个对象引用地址,所以 l2 和 l3 指向同一个对象。 第二题: 会报错, d['b'] = 20 这个已经不在函数内,所以此时会报d未定义的错误。 如果此时定义的函数如下: def func(d): d['a'] = 10 d['b'] = 20 由于外部调用的 d是可变变量,所有最后得到的结果会是: {'a':10, 'b':20}
    展开
  • 晁生贵
    2022-08-31 来自广东
    第一个 ,都是指向同一个对象 第二个,打印 {'a': 10, 'b': 20}
  • Geek_145846
    2022-04-26
    特别喜欢老师的专栏,把很多教程中没有说透的知识点说清楚了!
  • 徐李
    2022-04-15
    这个参数传递,python和java 简直一样