Python函数中修改变量(值传递、引用传递、可变对象、不可变对象)的深入思考

在Python中,如果将数字、字符串等传入到函数中,再改变其值,我们会发现:当函数结束时,该变量不会发生改变。

In [20]: def q(x):
    ...:     x = 2
    ...:

In [21]: a = 1

In [22]: q(a)

In [23]: a
Out[23]: 1

我们先令a=1,再在函数中让a的值变为2,当函数结束时,打印a的值,依旧为1。有人称类似于a这样的变量为不可变对象(数字、字符、字符串、元祖等)

In [24]: def p(x):
    ...:     x[0] = 2
    ...:

In [25]: b = [1]

In [26]: p(b)

In [27]: b
Out[27]: [2]

这里我们令b是只有一个元素,且该元素值为1的列表,在函数中我们修改了b[0]的值,退出函数后发现b的值被修改了。有人称类似于b这样的变量为可变对象(列表、字典等)。

我们再深入了解一下,利用id函数(用于求变量地址)

In [31]: def q(x):
    ...:     print(id(x))
    ...:     x = 2
    ...:     print(id(x))
    ...:
    ...:
    ...:

In [32]: a = 1

In [33]: id(a)
Out[33]: 2002568880

In [34]: q(a)
2002568880
2002568912

In [35]: id(a)
Out[35]: 2002568880

这里输出了4个地址,分别是调用函数前a的地址,传入函数时x的地址,x改变后的地址,以及调用结束后a的地址。其中,第三个地址与其他几个地址不同。这里你需要知道,在Python中,同样的值对应唯一一个地址,我们可以看看

In [36]: id(1)
Out[36]: 2002568880

In [37]: c=1

In [38]: id(c)
Out[38]: 2002568880

那么,在函数中发生了什么?我将用下面的图来表示(用画图画的,有点丑):

Python函数中修改变量(值传递、引用传递、可变对象、不可变对象)的深入思考_第1张图片

有一个内存块,地址是2002568880,该内存块上保存的值是1,我们定义了一个变量,其值为1,也就是让这个变量指向了地址为2002568880的内存块上。

此时,我们调用函数q,它生成了一个新变量x,这个x指向的地址和a指向的地址一样(这里我又画了一个内存块,储存的值为2)

Python函数中修改变量(值传递、引用传递、可变对象、不可变对象)的深入思考_第2张图片

在函数中,我们令x的值为2,也就是让x指向了内存地址为2002568912的内存块,但是请注意,a的指向并未发生改变。如下图

Python函数中修改变量(值传递、引用传递、可变对象、不可变对象)的深入思考_第3张图片

所以我们退出函数时,a的值未发生改变。

同样的,我们对b进行分析。假设b是一个长度为100的列表,其中

In [60]: b[0] = 1

In [61]: b[1] = 2

In [62]: b[99] = 2

我们再看看这三个元素的地址

In [63]: id(b[0])
Out[63]: 2002568880

In [64]: id(b[1])
Out[64]: 2002568912

In [65]: id(b[99])
Out[65]: 2002568912

和之前的1、2的地址完全相同!更加说明了对于任意一个数,地址是唯一的。

也就是说,数组的表示形式应该是如下图所示,b指向的是一个内存块,这个内存块存储了b[0]到b[99]的所有地址。

Python函数中修改变量(值传递、引用传递、可变对象、不可变对象)的深入思考_第4张图片

此时,定义一个函数p,修改b[99]的值

In [73]: def p(x):
    ...:     x[99] = 1
    ...:
    ...:

In [74]: b[99]
Out[74]: 2

In [75]: p(b)

In [76]: b[99]
Out[76]: 1

b[99]的值被修改了。

发生了什么?

在调用函数的过程中,首先生成x变量,指向了b指向的内存块。

Python函数中修改变量(值传递、引用传递、可变对象、不可变对象)的深入思考_第5张图片

再对x[99]赋值,指向了1所在的内存块

Python函数中修改变量(值传递、引用传递、可变对象、不可变对象)的深入思考_第6张图片

退出函数之后,x被丢弃,但b依然指向这个列表,而这个列表的内部元素已经被修改了。所以b[99]变成了1。

 

所以大家应该明白了,为什么数字、字符、字符串、元祖都是值传递,因为你传递进去生成了一个新的变量x,你修改x的值无非是修改了x指向的内存块,而对原变量没有影响。

而对于列表,字典这种能增删的变量类型,通过新变量x能修改内部元素指向的地址(但不改变x的指向),因而也会导致原变量被修改。

所以,抛弃掉可变对象,不可变对象,值传递,引用传递这些概念吧,你需要理解的是函数调用时的机制:有一实际参数a,在调用时产生一个新的变量x(形式参数),x和a指向相同的地址。如果对x赋值,意味着改变了x指向的内存块,而不改变a的值。如果x是列表,对x[0]赋值,则改变了x[0]指向的内存块,而又因为x[0]的地址是存放在列表中,a又指向了这个列表,因此a也被修改了。

如果你理解了上面,那么下面的情况你也应该理解了。

In [77]: def t(x):
    ...:     x = [55,66]
    ...:

In [78]: c = [1,2,3]

In [79]: t(c)

In [80]: c
Out[80]: [1, 2, 3]

你需要记住的是对x赋值,相当于改变了x指向的内存地址。而对c无关。

你可能感兴趣的:(Python)