Python 中一切皆 对象,而对象又分为 可变类型不可变类型。当将不同类型的对象作为参数传入函数时,往往会产生不同的效果。究其原因,是由于不同类型的对象有着各自不同的特性所导致的。

如果你有其他编程语言的基础,比如 CC++JavaScript 等,那么你也许听说过两种常见的函数传参方式:值传递(pass by value)、引用传递(pass by reference)。

Python 中函数参数的传递方式与大多数编程语言并不相同,既不是 值传递,也不是 引用传递。至于到底使用何种方式进行参数传递,等你阅读完本文就会明白了。不过,在此之前,我们先通过 JavaScript 代码来展示下到底什么是 值传递,什么是 引用传递,这样有助于我们理解 Python 中函数参数的传递方式。

值传递

JavaScript 中,如果函数接收到的参数是原始值类型(数值、字符串、布尔值等),函数参数的传递方式即为 值传递值传递 所表现出来的结果就是:在函数体内部修改传递进来的参数的值,并不会影响函数体外部原对象的值。

1
2
3
4
5
6
7
8
var a = 1;

function foo(p) {
p += 2;
}

foo(a);
console.log(a); // 1

以上代码中,在函数体内部对变量 p 的值进行了修改,但是并没有影响函数外部原有变量 a 的值。由于是 值传递,所以 p 的值是对原有变量 a 的值的一份拷贝,修改 p 也就不会对 a 产生任何影响。

引用传递

如果函数接收到的参数是引用类型(数组、对象等),函数参数的传递方式即为 引用传递。而 引用传递 所表现出来的结果是:在函数体内部修改传递进来的参数的值,将会直接改变函数体外部原对象的值。

1
2
3
4
5
6
7
8
var arr = [1, 2, 3];

function bar(p) {
p.push(4);
}

bar(arr);
console.log(arr); // [1, 2, 3, 4]

在这段代码中,同样在函数体内部对变量 p 的值进行了修改,函数外部原有变量 arr 的值跟着一起被改变了。这是因为 p 的值是对原有变量 arr 的值的引用,它们俩实际上指向同一个对象 [1, 2, 3]。只要修改一个变量的值,另一个变量的值就会跟着改变。

值得注意的是,如果将上面的代码稍作修改,改为如下的代码。其结果表现出来的又像是 值传递 的特性。

1
2
3
4
5
6
7
8
9
var arr = [1, 2, 3];

function bar(p) {
p = [1, 2, 3, 4];
p.push(5);
}

bar(arr);
console.log(arr); // [1, 2, 3]

实际上这个示例函数参数的传递方式依然是 引用传递。出现以上结果的原因是,在函数体内部,传递进来的 arr 的值实际上被整体替换成了另一个数组 [1, 2, 3, 4],现在 p 所指向的对象是新的数组 [1, 2, 3, 4],而不是函数体外部 arr 所指向的数组 [1, 2, 3] 了。

Python 中变量与赋值

搞明白了什么是 值传递,什么是 引用传递,我们回到 Python 语言中来。在正式介绍 Python 中函数参数的传递方式之前,让我们先来回顾下什么是 Python 中的 变量,及 赋值 操作。

先来看一小段示例代码。

1
2
3
4
5
6
7
>>> a = 1
>>> b = a
>>> a = a + 2
>>> a
3
>>> b
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 依然在那里。

可以通过流程图来看下整个代码执行过程。

a = 1
a = 1
b = a
b = a
a = a + 2
a = a + 2

以上就是 Python 中创建 不可变类型 对象,并将其 赋值变量 的过程。而对于 可变类型 对象的操作会产生不同的结果,下面通过对一个 list 对象操作进行演示。

1
2
3
4
5
6
7
>>> a = []
>>> b = a
>>> a.append(1)
>>> a
[1]
>>> b
[1]

前面两行代码 a = []b = a 与之前 不可变类型 对象的例子一样,只不过现在 ab 两个标签都贴在了可变对象 [] 上。当执行 a.append(1) 的时候,由于 list可变类型, 所以无需再次创建一个新的列表对象,而是直接在原对象上面做更改操作,因此标签 a 仍然贴在这个对象上,ab 最终依然指向的是同一个 list 对象,并且这个对象由原来的 [] 变成了 [1]

我们还是通过流程图来看下整个代码执行过程。

a = []
a = []
b = a
b = a
a.append(1)
a.append(1)

由以上两个示例可以看出,实际上在 Python 中 变量 本身并没有所谓的 类型,我们可以把它当作 标签 来理解,而 变量 所指向的 对象 才有 类型 的区分。对于 不可变类型 来说,改变 变量 的值,实际上是在对 变量 作重新赋值的操作;而对于 可变类型,改变 变量 的值,实际上是直接修改它所指向的 对象 的值。

Python 中函数参数的传递方式

经过了前面的铺垫,现在可以正式介绍 Python 中函数参数的传递方式了。其实 Python 中唯一支持的函数参数的传递方式叫 对象的引用传递(call by object reference)。函数的 形参 将获得传递进来的 实参 的引用副本。

结合 Python 中变量与赋值就很容易理解这种传参方式了。Python 中一切皆 对象,而 变量对象 的关系是,变量 会通过指向一个 对象 来引用这个 对象 的值,所以函数传参时,并不需要根据数据类型来决定使用 值传递 还是 引用传递,只需要将 实参变量 的引用拷贝一份给 形参变量 就可以了,即让 形参实参 指向同一个对象。

下面分别来看两个示例,第一个示例传递给函数 不可变类型 参数,第二个示例传递给函数 可变类型 参数。

1
2
3
4
5
6
7
a = 1

def foo(p):
p += 2

foo(a)
print(a) # 1
1
2
3
4
5
6
7
li = [1, 2, 3]

def bar(p):
p.append(4)

bar(li)
print(li) # [1, 2, 3, 4]

两个示例的打印结果也证实了上面我们对 Python 中函数参数传递方式的分析。实际上,拿这两段示例代码跟最开始介绍 值传递引用传递 时所写的 JavaScript 代码进行比较,你会发现其实它们得到的竟然是同一种结果。

无论在 Python 中,还是在 JavaScript 中,对于 可变类型不可变类型 当作参数传入函数时,执行完函数体内部的代码后,最终体现出来的结果是相同的。但你要明白,这两种语言对于函数参数传递方式的处理机制是完全不同的,只不过它们最终体现出来的结果相同。