对Python函数传参的一些思考

今天看到这样一个问题: Python 的函数是怎么传递参数的,有了一些兴趣,因为以前都是直接信的一个流传度较广的说法

对于不可变对象作为函数参数,相当于C系语言的值传递;

对于可变对象作为函数参数,相当于C系语言的引用传递。

那么事实上真是如此吗?我们来看几个例子。

首先我们来看看第一个说法,这里我实验环境是Python3.6。

对于不可变对象作为函数参数进行传递,这里我分别以inttuple进行测试

def test_immutable_for_simpledata(args):
    print('============')
    print(2, id(args))
    args = args + 10
    print(3, args)
    print(4, id(args))
    print('============')

if name == 'main': cur = 5000 print(1, id(cur)) test_immutable_for_simpledata(cur) print(5, cur) print(6, id(cur))

结果如下
1 42301856
============
2 42301856
3 5010
4 42301616
============
5 5000
6 42301856
我们看第一步和第二步,两者打印出来的地址是一样的,这可以证明Python对不可变类型传参是使用的引用传参,但是为什么函数中用了args+10,函数外面的args值还是没变呢?我们可以看到第四步的args的地址其实已经变了。即说明加10和未加10两个时间点是两个不同的对象。为什么是这样呢?这就需要理解什么是不可变对象了。在我理解来,不可变对象就是它的成员元素不变,比如对于简单对象(如int,float)来说,就是它的值不变,对于复合类型(如tuple)来说,就是它的成员元素(即地址)不变。回到代码中,这里是基本类型的不可变对象,对它进行运算操作其实就是创建新的对象,然后将原先的变量名绑定到新的对象上。读者前一句话没读懂的话,可以去了解一下Python的名字空间。

为了加深读者的理解,我们再以tuple为例进行测试。代码如下

def test_immutable_for_tuple(args):
    print('============')
    print(2, id(args))
    args[0].append(5)
    print(3, args)
    print(4, id(args))
    print('============')
if name == 'main':
    cur = ([0], 1)
    print(1, id(cur))
    test_immutable_for_tuple(cur)
    print(5, cur)
    print(6, id(cur))
结果如下
1 33951020
============
2 33951020
3 ([0, 5], 1)
4 33951020
============
5 ([0, 5], 1)
6 33951020

这次的结果和上次明显不同了。虽然tuple是不可变对象,我们使用tuple为参数传入函数中,然后对tuple中的列表元素进行了操作,结果函数外部的变量也发生了变化。我们也可以看到参数的地址在函数内外其实都一样。

根据上述测试,我们可以得出一个结论,对于不可变对象的函数传参,依然是传的引用(地址)。对于简单类型,在函数内对其操作之所以不会影响函数范围外的值,是因为运算中的赋值操作产生了新的对象,而不是对原有对象的改变。而对于一些复杂类型,就可以比较清晰的看出,函数内参数的改变同样会影响函数外的变量。

对于可变对象,我们同样可以使用上述逻辑去验证,由于篇幅,我就不啰嗦了。

最终可以得到的结论是,Python中的函数通过引用传参。