Python 中一切皆 对象,而对象又分为 可变类型 和 不可变类型。当将不同类型的对象作为参数传入函数时,往往会产生不同的效果。究其原因,是由于不同类型的对象有着各自不同的特性所导致的。
如果你有其他编程语言的基础,比如 C、C++、JavaScript 等,那么你也许听说过两种常见的函数传参方式:值传递(pass by value)、引用传递(pass by reference)。
Python 中函数参数的传递方式与大多数编程语言并不相同,既不是 值传递,也不是 引用传递。至于到底使用何种方式进行参数传递,等你阅读完本文就会明白了。不过,在此之前,我们先通过 JavaScript 代码来展示下到底什么是 值传递,什么是 引用传递,这样有助于我们理解 Python 中函数参数的传递方式。
值传递
在 JavaScript 中,如果函数接收到的参数是原始值类型(数值、字符串、布尔值等),函数参数的传递方式即为 值传递。值传递 所表现出来的结果就是:在函数体内部修改传递进来的参数的值,并不会影响函数体外部原对象的值。
1 | var a = 1; |
以上代码中,在函数体内部对变量 p 的值进行了修改,但是并没有影响函数外部原有变量 a 的值。由于是 值传递,所以 p 的值是对原有变量 a 的值的一份拷贝,修改 p 也就不会对 a 产生任何影响。
引用传递
如果函数接收到的参数是引用类型(数组、对象等),函数参数的传递方式即为 引用传递。而 引用传递 所表现出来的结果是:在函数体内部修改传递进来的参数的值,将会直接改变函数体外部原对象的值。
1 | var arr = [1, 2, 3]; |
在这段代码中,同样在函数体内部对变量 p 的值进行了修改,函数外部原有变量 arr 的值跟着一起被改变了。这是因为 p 的值是对原有变量 arr 的值的引用,它们俩实际上指向同一个对象 [1, 2, 3]。只要修改一个变量的值,另一个变量的值就会跟着改变。
值得注意的是,如果将上面的代码稍作修改,改为如下的代码。其结果表现出来的又像是 值传递 的特性。
1 | var arr = [1, 2, 3]; |
实际上这个示例函数参数的传递方式依然是 引用传递。出现以上结果的原因是,在函数体内部,传递进来的 arr 的值实际上被整体替换成了另一个数组 [1, 2, 3, 4],现在 p 所指向的对象是新的数组 [1, 2, 3, 4],而不是函数体外部 arr 所指向的数组 [1, 2, 3] 了。
Python 中变量与赋值
搞明白了什么是 值传递,什么是 引用传递,我们回到 Python 语言中来。在正式介绍 Python 中函数参数的传递方式之前,让我们先来回顾下什么是 Python 中的 变量,及 赋值 操作。
先来看一小段示例代码。
1 | a = 1 |
在 Python 中,a = 1 这句代码实际上执行效果是先在内存中创建了一个 int 类型的对象 1,然后将对象 1 赋值给变量 a。变量 a 就是对象 1 的一个标识,我们可以把它比作一个 标签。当执行 b = a 的时候,可以理解为给对象 1 又贴了一个新的标签 b。当代码执行到 a = a + 2 时,由于 int 类型对象是不可变的,即不能在原对象 1 上做原地加 2 的操作,所以相当于将对象 1 上的标签 a 撕了下来,贴在了一个新的对象 3 上,而对象 1 上原来贴的标签 b 依然在那里。
可以通过流程图来看下整个代码执行过程。
以上就是 Python 中创建 不可变类型 对象,并将其 赋值 给 变量 的过程。而对于 可变类型 对象的操作会产生不同的结果,下面通过对一个 list 对象操作进行演示。
1 | a = [] |
前面两行代码 a = [] 和 b = a 与之前 不可变类型 对象的例子一样,只不过现在 a、b 两个标签都贴在了可变对象 [] 上。当执行 a.append(1) 的时候,由于 list 是 可变类型, 所以无需再次创建一个新的列表对象,而是直接在原对象上面做更改操作,因此标签 a 仍然贴在这个对象上,a 和 b 最终依然指向的是同一个 list 对象,并且这个对象由原来的 [] 变成了 [1]。
我们还是通过流程图来看下整个代码执行过程。
由以上两个示例可以看出,实际上在 Python 中 变量 本身并没有所谓的 类型,我们可以把它当作 标签 来理解,而 变量 所指向的 对象 才有 类型 的区分。对于 不可变类型 来说,改变 变量 的值,实际上是在对 变量 作重新赋值的操作;而对于 可变类型,改变 变量 的值,实际上是直接修改它所指向的 对象 的值。
Python 中函数参数的传递方式
经过了前面的铺垫,现在可以正式介绍 Python 中函数参数的传递方式了。其实 Python 中唯一支持的函数参数的传递方式叫 对象的引用传递(call by object reference)。函数的 形参 将获得传递进来的 实参 的引用副本。
结合 Python 中变量与赋值就很容易理解这种传参方式了。Python 中一切皆 对象,而 变量 与 对象 的关系是,变量 会通过指向一个 对象 来引用这个 对象 的值,所以函数传参时,并不需要根据数据类型来决定使用 值传递 还是 引用传递,只需要将 实参变量 的引用拷贝一份给 形参变量 就可以了,即让 形参 与 实参 指向同一个对象。
下面分别来看两个示例,第一个示例传递给函数 不可变类型 参数,第二个示例传递给函数 可变类型 参数。
1 | a = 1 |
1 | li = [1, 2, 3] |
两个示例的打印结果也证实了上面我们对 Python 中函数参数传递方式的分析。实际上,拿这两段示例代码跟最开始介绍 值传递 和 引用传递 时所写的 JavaScript 代码进行比较,你会发现其实它们得到的竟然是同一种结果。
无论在 Python 中,还是在 JavaScript 中,对于 可变类型 和 不可变类型 当作参数传入函数时,执行完函数体内部的代码后,最终体现出来的结果是相同的。但你要明白,这两种语言对于函数参数传递方式的处理机制是完全不同的,只不过它们最终体现出来的结果相同。