引言

研究此问题的出发点是希望在Python中传递函数参数的时候,能够像C++中的按引用传递参数的方式进行传递,函数内部对参数所做的修改能够反映到被调函数外部的传入变量本身去。

经过一番研究,发现Python中的变量与C中的变量不太一样,关键在于以下两点:

  • 变量都是对内存中对象的引用,但这种引用不是单对单的映射

  • 对象分为不可变对象(整数、实数、字符串和元组)和可变对象(字典、列表、集合和类实例等)

验证与说明

我们借助id()函数来说明上述两点,下述是文档中对id()函数的说明:

id(object)

Return the “identity” of an object. This is an integer (or long integer) which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same id() value.

CPython implementation detail: This is the address of the object in memory.

简而言之id(object)函数返回对象在生命周期内在内存中的唯一标识。

不可变对象

首先看不可变对象,这种类型的变量的值如果发生了改变,不会对原本的内存对象进行改动,而是令变量指向了一个新的对象。以整型为例:

>>> a = 3
>>> b = a
>>> print id(a), id(b)
94531587187000 94531587187000

我们可以看到,上述两个变量ab是对同一个内存对象的引用。但这样的话,如果我们对变量b进行修改,变量a岂不是也要随之改变?这不符合我们的认知,来看实际是怎样:

>>> b = 4
>>> a, b
(3, 4)
>>> print id(a), id(b)
94531587187000 94531587186976

我们可以看到对b的修改并没有影响到a,此时b指向了一个新的对象。那么我们此时修改只有一个引用的变量a,结果是怎样的呢?

>>> a += 1
>>> print id(a)
94531587186976

此处有两点要注意:

  • 对变量自增也改变了所指向的内存对象,这就是上述所说的不可变对象的含义,整型类型的变量值如果发生了改变,不会对原本的内存对象进行改动,而是令变量指向了一个新的对象。
  • 此处变量ab的值都是4,自增后a的对象标识同变量b又重合了,这也超出了我原本的预期。关于这一点经初步验证,整型和字符串有这种特性——即使不是直接通过a = b类的赋值,相同的值的变量会指向同一内存对象。

可变对象

可变对象更符合我从C语言得来的对于变量的认知,对于变量所做出的修改是直接对所指向的内存的修改:

>>> a = [0, 1, 2]
>>> id(a)
139798883952328
>>> a[0] = 3
>>> a
[3, 1, 2]
>>> id(a)
139798883952328

可以看到,对于列表类型变量的修改并没有改变变量所指向的内存对象。

并且对于指向相同的两个变量来说,其中一个变量改变也会导致另一个变量的改变,两个变量所指向的对象仍然不会变动:

>>> a = [1,2,3]
>>> id(a)
139995714246344
>>> b = a
>>> id(b)
139995714246344
>>> b[0] = 0
>>> a
[0, 2, 3]
>>> b
[0, 2, 3]

函数传参

讲到这里,对于python中的传参问题其实也比较清楚了:python中的变量全都是引用,所以函数传参都是传的引用过去。

但是对于不可变对象类型的变量来说,有些类型(字符串,元组)的变量本身就不可更改;有的则是更改值会使其函数的局部变量指向另外的内存对象,就相当于C语言中函数传递了指针作为参数,指针本身是按值传递的,在函数内部更改指针所指向的地址不会影响函数外部变量的值,所以从效果上来说与按值传递是一样的。

而对于可变对象类型的变量来说,对它们的修改会直接影响到所指向的内存对象,所以与按引用传递的效果是一样的。