python的所有变量都是引用类型,也就是说每个变量至少包括两个部分:存放变量引用的内存块和存放变量内容的内存块
以字符串为例
由于字符串是不可变类型,如果改变已经创建字符串的内容,将执行替换操作,将变量指向一块新的内存。
s = 'abcd'
print(s,':',id(s))
s = 'ijmn'
print(s,':',id(s))
abcd : 1390779247728
ijmn : 1390779248120
再来看看可变类型list
上图展示了append和重新对元素赋值两个操作,都不改变lst的地址指向。但是lst[2]的指向改变(因为替换操作)。现在可以回头来看函数传参的问题
当我们传递一个参数给函数时候,函数将会对传入参数的地址创建一个副本。
圆圈2为传参时函数建立的传入变量(圆圈1)的副本,他们拥有共同的指向(圆圈3)。那么显然在函数中,如果将副本重新指向一块新的内存,传入变量(圆圈1)的指向并不会改变,换句话说这种操作并不能影响函数外的变量。
那么什么操作会影响传入变量呢?
因为副本和传入变量拥有相同的内存指向,如果我们在函数中对这块共同指向的内存(圆圈3)做改变,那么改变就将会影响传入变量,比如对list变量进行append操作。
再来思考传入可变与不可变变量的区别。
因为改变不可变变量时,通常会使得副本重新指向新的内存块,所以这种操作不会影响传入变量。但是也有例外,比如tuple变量中包含list变量,在函数中对传入的tuple变量中的list元素进行操作,将会影响传入变量的值(这可以从上述方法中得到解释)。
var = (10, [0, 0])
def func(var):
var[1].append(1)
print('in function:', var)
func(var)
print('global:',var)
in function: (10, [0, 0, 1])
global: (10, [0, 0, 1])
顺带提一提类中的实例变量与类变量的操作问题
class A:
num = 10
lst = [0]
def f(self):
self.num = 100
self.t = 100
def f2(self):
self.lst.append(1)
a = A()
print('a.num:', a.num, 'A.num:', A.num)
a.f()
print('impact of f()')
print('a.num:', a.num, 'A.num:', A.num)
print('\n')
print('a.lst:', a.lst, 'A.lst:', A.lst)
a.f2()
print('impact of f2()')
print('a.lst:', a.lst, 'A.lst:', A.lst)
原则上讲类变量应该用类名字引用,但是用实例引用(self.var)的方式引用也并不会报错。程序结果如下:
a.num: 10 A.num: 10
impact of f()
a.num: 100 A.num: 10a.lst: [0] A.lst: [0]
impact of f2()
a.lst: [0, 1] A.lst: [0, 1]
这说明用实例引用类变量的时候,实际上也是创建了一个副本,走的就是函数传参的那一套。
拷贝问题
主要有三个方式:
- 建立副本 (传参和复制)
a = [1, 2, [0]]
b = a
b[0] = 100
print(a)
b的改变将影响a
- 浅拷贝(拷贝一层引用)
a = [1, 2, [0]]
b = a.copy()#b = a[:]
b[0] = 'b'
print('a:', a)
print('b:', b)
a: [1, 2, [0]]
b: [‘b’, 2, [0]]
a = [1, 2, [0]]
b = a.copy()#b = a[:]
b[2][0] = 'b'
print('a:', a)
print('b:', b)
a: [1, 2, [‘b’]]
b: [1, 2, [‘b’]]
可以看到,一层拷贝保证了第一层引用的改变不会相互影响,但是第二层改变是将影响被拷贝对象。
- 深拷贝(将原变量的结构完全复制一遍)
import copy
a = [1, 2, [0]]
b = copy.deepcopy(a)#b = a[:]
b[2][0] = 'b'
print('a:', a)
print('b:', b)
a: [1, 2, [0]]
b: [1, 2, [‘b’]]
深拷贝是将原变量内容结构完全复制一遍到新的空间,两者的内存指向已经完全不沾边了,所以怎么改变都不会相互影响。