無知識 | 三無計劃

Python 中的变量传递方式

之前一直没有仔细思考过这个问题,直到最近用到方才深入了解。其实并不复杂,和 C++ 如出一辙。

Python 中的变量

简而言之,Python 中的变量都是对实体的引用,但并不是实体本身;此外,Python 的函数传参仍然遵循按值传递,结合这两点就能搞明白 Python 的传参行为。

先抛开具体的编程语言,如果有以下伪代码:

a=1
a=2

多数语言会把这两行解释为:声明一个变量 a,在内存里分配空间,这个空间就叫 a,里面存 1;然后把这个空间里的值更新成 2。

但是 Python 中对变量(其实是对象)的处理有所不同。以上两行在 Python 中代表:

  1. 创建一个对象(整数 1),放在内存里,再创建一个引用,这个引用叫做 a,指向之前创建的对象
  2. 又创建一个对象(整数 2),放在内存里,把 a 指向这个新的对象

看到不同了吧?接下来讨论函数传参。

Python 中的传参

在 Python 中的函数传参是依照值传递的;但由于 Python 中的变量都是引用,因此就有了以下这句有点绕的话:

Python passes references-to-objects by value (like Java), and everything in Python is an object.

看以下代码段

def func(a: list):
    print(a)
    a.append(2)
    print(a)
a=[1]
func(a)
print(a)

输出:

[1]
[1, 2]
[1, 2]

调用 func 并把 a 传入时,Python 在局部新建了一个局部的变量 a,并把外部 a 的值赋值给了新的变量。由于 a 是引用,新的变量也是引用,都指向内存中的同一个列表对象,因此在函数内部对列表进行的操作也同样可以由外部的 a 观测到。

然而,要注意两个陷阱:对引用进行 re-assign;以及对 immutable 对象的处理。

对引用 re-assign

修改以上代码为:

def func(a: list):
    print(a)
    a=['a'] # 注意这里
    print(a)
a=[1]
func(a)
print(a)

输出:

[1]
['a']
[1]

为什么函数内对 a 的修改不能被外部的 a 观测到?

a=['a'] 这行代码并不是通过 a 修改 a 指向的 list,而是在内存中新建了一个列表对象 ['a'],并将函数内部的 a 指向了这个新的对象,并且此后的操作都针对这个新的列表对象;然而由于函数内部的 a 与外部的 a 是两个不同的变量,因此外部的 a 仍然指向之前的 list。这种操作称为 re-assign,需要与 append 这种针对对象本身的方法区别开。

immutable 对象的处理

如果不使用列表,而使用字符串:

def func(a: str):
    print(a)
    a='changed'
    print(a)
a='original'
func(a)
print(a)

结果:

original
changed
original

如果理解了前文的要点,那么针对 immutable 对象的处理并不难懂。由于 str 对象是 immutable 的(不可变对象),因此函数内部不能通过 a 修改指向的字符串,a='changed' 这行代码实际是在进行 re-assign,也就是新建了一个字符串对象 'changed',使函数内部的 a 指向这个新的对象。


总而言之记住三点:

  1. Python 中的变量是对象的引用,而不是对象本身
  2. Python 按值传参
  3. 警惕对引用进行 re-assign 与通过引用修改对象的区别