Python的参数传递

159 阅读7分钟

值传递和引用传递

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

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

Python变量及其赋值

a = 1
b = a
a = a + 1

这里首先将 1 赋值于 a,即 a 指向了 1 这个对象,如下面的流程图所示:

4d714d3a5f895b41604685905f5e42ac.png

接着 b = a 则表示,让变量 b 也同时指向 1 这个对象。这里要注意,Python 里的对象可以被多个变量所指向或引用。

002055603315945962330d64227552b5.png

最后执行 a = a + 1。需要注意的是,Python 的数据类型,例如整型(int)、字符串(string)等等,是不可变的。所以,a = a + 1,并不是让 a 的值增加 1,而是表示重新创建了一个新的值为 2 的对象,并让 a 指向它。但是 b 仍然不变,仍然指向 1 这个对象。

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

9f9017703521133b2819929e998d62b4.png

通过这个例子你可以看到,这里的 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]这个对象。

6d532905f120237b440f8f1c1a147226.png

由于列表是可变的,所以 l1.append(4) 不会创建新的列表,只是在原列表的末尾插入了元素 4,变成[1, 2, 3, 4]。

由于 l1 和 l2 同时指向这个列表,所以列表的变化会同时反映在 l1 和 l2 这两个变量上,那么,l1 和 l2 的值就同时变为了[1, 2, 3, 4]。

56be765f6c39824e27acb5ec6471bff5.png

需要注意的是,Python 里的变量可以被删除,但是对象无法被删除。

l = [1, 2, 3]
del l

del l 删除了 l 这个变量,从此以后你无法访问 l,但是对象[1, 2, 3]仍然存在。

Python 程序运行时,其自带的垃圾回收系统会跟踪每个对象的引用。如果[1, 2, 3]除了 l 外,还在其他地方被引用,那就不会被回收,反之则会被回收。

  • 变量的赋值,只是表示让变量指向了某个对象,并不表示拷贝对象给变量;而一个对象,可以被多个变量所指向。
  • 可变对象(列表,字典,集合等等)的改变,会影响所有指向该对象的变量。
  • 对于不可变对象(字符串、整型、元组等等),所有指向该对象的变量的值总是一样的,也不会改变。但是通过某些操作(+= 等等)更新不可变对象的值时,会返回一个新的对象。
  • 变量可以被删除,但是对象无法被删除。

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。

 def my_func2(b):
  b = 2
  return b

a = 1
a = my_func2(a)
a
2

让函数返回新变量,赋给 a。这样a 就指向了一个新的值为 2 的对象,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]

这里 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

l2和l3是指向同一个对象,因为两者之间用等号赋值了,l1并不是,l1所指向的[1, 2, 3]是另外一块内存空间,大家可以通过id()这个函数验证


第二个问题,下面的代码中,打印 d 最后的输出是什么呢?

def func(d): 
  d['a'] = 10 
  d['b'] = 20
  
d = {'a': 1, 'b': 2}
func(d)
print(d)

输出的是{'a': 10, 'b': 20},字典是可变的,传入函数后,函数里的d和外部的d实际上都指向同一个对象 d[idx] = value语句改变了字典对应key所指向的值

学习资料:极客时间《Python 核心技术与实战》