先来看这一段程序:
a = [1,2,3,4]
b = a
a[0] = 100
print(b)
输出结果是:
[100, 2, 3, 4]
看上去很简单,但我发现一些教科书、Python课程,以及同行上课时并不会提到这样的例子,更不用说解释为什么了。Python中所有的变量,都是指针。非计算机专业的编程学习者对变量是指针还是实体这个问题不敏感,如果不和他们讲清楚这一点,碰到类似上面那样的程序,他们是没法理解的,也许他们就会被类似的问题困扰一辈子。本着对Python初学者人生负责的态度,我觉得这个问题需要强调一下。
更广泛地说,可以把Python中所有可赋值的东西,都称为变量。所有的变量,都是指针。比如列表的元素是可赋值的,因此列表的元素就是指针。字典的值部分也是可赋值的,所以字典的值也是指针。指针的本质是内存地址。指针这个词太专业了,不好,不如称作箭头,一个指向内存单元中存放的数据的箭头。这么说,变量就是箭头,对变量进行赋值,就是将该箭头指向内存中的某处。当然其它程序设计语言中的变量,未必是上述的情况。
a = 3
b = 4
上面两条赋值语句的效果,可以理解为下图:
a是个箭头(指针),指向内存中某处存放的3,b也一样,它指向4。
用一个变量对另一个变量进行赋值,就是让两个变量指向相同的地方。因此,若再执行:
a = b
产生的效果如下:
a指向了b指向的地方,所以a的值也变成4。我们说变量a的值是4,归根到底是在说, a指向4。
Python中有两个运算符,"is"和"==",含义有所不同,但有些类似。a is b为True,说的是a和b指向同一个地方; 而 a == b 为True,说的是a和b指向的地方的内容相同,但a和b 未必指向同一个地方。例如:
1. a = [1,2,3,4] #a指向列表 [1,2,3,4]
2. b = [1,2,3,4] #b指向另一个列表 [1,2,3,4]
3. print( a == b) #True
4. print( a is b) #False
5. c = a
6. print( a == c) #True
7. print( a is c) #True
8. a[2] = "ok"
9. print(c) #[1, 2, 'ok', 4]
上面程序执行完第5行时,效果如下图:
内存中有两份列表[1,2,3,4],a和b分别指向它们。因此a和b指向不同的地方,但是它们指向的地方存放的内容是一样的。故第3行输出True而第4行输出False。第5行使得c与a指向同一份列表。因此第6、7行都输出True。
第8行,修改了a[2],情况变为下图:
因为c和a指向同一个地方,所以a的内容变了,c的内容自然也变。所以输出c,结果就是 [1, 2, 'ok', 4]。
对int,float,complex, str,tuple类型的变量a和b,只需关注 a == b是否成立,一般不需要关注 a is b是否成立。因这些数据本身都不会更改,不会产生上面第8行那样,a指向的内容改了b指向的内容也跟着变的情况。
对 list,dict,set类型的变量a和b, a == b和 a is b的结果都需要关注。因这些数据本身会改变。改变了a指向的内容,说不定b指向的内容也变了。
因为列表的元素可以被赋值,因此,列表的元素其实也是箭头。
a = [1,2,3,4]
b = [1,2,3,4]
上面这两条语句,准确的效果如下图:
a和b的每个元素,如a[0],b[1],都是箭头。a[0]和b[0]没有分别指向不同的两个1,是因为1本身不可变,没有必要保有两份。若对a[0]进行赋值,那就是让a[0]指向了别处,而不是将a[0]所指向的那个1改成别的什么。所以,假如a[0]被赋成别的值,b[0]并不会受影响,它仍然指向1。
Python函数的参数也是箭头。Python函数的参数(形参),是实参(实际调用时给的参数)的拷贝。拷贝的意思是,形参和实参指向同一个地方。对形参赋值(让其指向别处)不会影响实参。例如:
1. def Swap(x,y):
2. tmp = x
3. x = y
4. y = tmp
5. a,b = 4,5
6. Swap(a,b)
7. print(a,b) #输出 4, 5
进入Swap函数时,x等于a,y等于b。Swap函数执行过程中交换了x,y的值,但这并不会影响a和b。在函数中的 tmp = x 刚执行完时,效果如下:
x,y分别是a和b的拷贝,即x和a同指向4,y和b同指向5。tmp = x使得tmp也指向4。Swap函数执行完后,x和y的值交换了,本质上是说x和y交换了它们的指向,因此情况变成下图:
显然,a和b的指向不会发生任何变化,它们的值自然不变。
但是如果函数执行过程中,改变了形参所指向的地方的内容,则实参所指向的地方内容也会被改变。例如:
1. def Swap(x,y):
2. tmp = x[0]
3. x[0] = y[0]
4. #若x,y是列表,则x[0],y[0]都是箭头
5. y[0] = tmp
6. a = [4,5]
7. b = [6,7]
8. Swap(a,b)
9. #进入函数后,x和a指向相同地方,
10. #y和b指向相同地方
11. print(a,b) #>>[6, 5] [4, 7]
这个程序中,Swap(a,b)使得a和b的下标为0的元素发生了交换。这是因为,x和a指向同一张列表[4,5],y和b指向同一张列表[6,7]。因此x[0]就是a[0],y[0]就是b[0]。进入Swap函数,执行完tmp = x[0] 时,情况如下:
Swap交换了x[0]和y[0],也就交换了a[0]和b[0]。因此该函数执行完时,情况如下:
由于a和x指向相同的地方,所以x[0]变了,a[0]自然也变了。b和y的关系亦然。
函数的返回值也是箭头。假设函数中的返回语句是 return x, 如果x是变量,则返回值和x指向相同的地方;如果x是一个非变量的表达式,那么返回值指向这个表达式计算出来后的值。可赋值的东西都是箭头,但是箭头未必都可赋值。例如函数的返回值,就是不可赋值的。比如f是个无参数的函数,f()的返回值(如果有的话)就是箭头。a = f() 是用f的返回值对a进行赋值,使得a和f的返回值指向同一个地方。但f()= 100 这种写法是不可行的。