面试的时候,有没有被问到Python传参是传引用还是传值这种问题?有没有听到过Python传参既不是传值也不是传引用这种说法?一个小小的参数默认值也可能让代码出现难以查找的bug?

如果你也遇到过上面的问题,不妨我们来探究下Python函数传递的种种。

万物皆对象

Python中有一个非常重要的概念——万物皆对象,无论是一个数字、字符串,还是数组、字典,在Python中都会以一个对象的形式存在。

1
a = 123

对于上面这行代码,在Python看来就是创建一个PyObject对象,值为123,然后定义一个指针a,a指向这个PyObject对象。

可变对象和不可变对象

Python中的对象分为两种类型,可变对象和不可变对象,不可变对象指tuple、str、int等类型的对象,可变对象指的是dict、list、自定义对象等类型的对象,我们用一段代码说明他们的区别。

1
2
3
4
5
6
7
8
9
a = [1, 2, 3]
print(id(a)) # 2587116690248
a += [4]
print(id(a)) # 2587116690248

b = 1
print(id(b)) # 2006430784
b += 1
print(id(b)) # 2006430816

上面代码中我们分别定义了一个可变对象和一个不可变对象,并且对他们进行修改,打印修改前后的对象标识可以发现,对可变对象进行修改,变量对其引用不会发生变化,对不可变对象进行修改,变量引用发生了变化。

可变对象的引用

上图是一个可变对象,当修改对象时,例如删除数组中的一个元素,实际上把其中一个元素从对象中移除,对象本身的标识是不发生变化的。

不可变对象的引用

改变一个不可变对象时,例如给一个int型加2,语法上看上去是直接修改了i这个对象,但是如前面所说,i只是一个指向对象73的一个变量,Python会将这个变量指向的对象加2后,生成一个新的对象,然后再让i指向这个新的对象。

参数传递时的表现

了解了对象的原理后,我们就可以来尝试理解一下参数传递时他们的不同表现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = [1, 2, 3]
print(id(a)) # 1437494204232
def mutable(a):
print(id(a)) # 1437494204232
a += [4]
print(id(a)) # 1437494204232
mutable(a)

b = 1
print(id(b)) # 2006430784
def immutable(b):
print(id(b)) # 2006430784
b += 1
print(id(b)) # 2006430816
immutable(b)

通过上面的代码可以看出,修改传进的可变参数时,会对外部对象产生影响,修改不可变参数时则不会影响。

概括地说,Python参数传递时,既不是传对象也不是传引用,之所以会有上述的区别,跟Python的对象机制有关,参数传递只是给对象绑定了一个新的变量(实际上是传递C中的指针)。

参数传递时的坑

理解了参数传递的逻辑,我们需要注意一下这种逻辑可能引发的问题。

1
2
3
4
5
6
7
def test(b=[]):
b += [1]
print(b)

test() # [1]
test() # [1, 1]
test() # [1, 1, 1]

上面的代码的输出,按照可变对象传参的逻辑,应该每次调用都输出[1]才对,而实际输出看上去好像默认参数好像只生效了一次。原因在于Python的函数也是对象(万物皆对象),这个对象只初始化一次,加上参数又是不可变对象,所以每次调用实际上都修改的是一个对象。

解决这个问题,推荐再参数传递可变对象时,默认值设置为None,在函数内部对None进行判断后再赋予默认值。

1
2
3
4
5
6
7
8
def test(b=None):
b = b or []
b += [1]
print(b)

test() # [1]
test() # [1]
test() # [1]

再看一段代码。

1
2
3
4
5
6
i = 1
def test(a=i):
print(a)

i = 2
test() # 1

由于参数默认值是在函数定义时而不是函数执行时确定的,所以这段代码test方法的参数默认值时1而不是2。