曾经看到这样一个问题,一个字典中的元素是列表,将这个列表元素赋值给一个变量,然后修改这个列表中元素的值,结果发现,字典中那个列表也同样修改了。那个问题如下:
-
dict = {'a':[1,2,3,4,5],'b':2}
-
x = dict['a']
-
for i in range(5):
-
x[i] = 0
-
print(dict['a'])
程序运行结果如下:
[0, 0, 0, 0, 0]
这儿涉及到Python赋值到底是引用还是拷贝一份的问题,即赋值时是传值还是传址。上面问题是将“a”的值赋给了x出现了上述情况,如果是将“b”的值赋给了x,当我们修改x的值时,字典dict的值并不受影响。
-
>>> dict = {'a':[1,2,3,4,5],'b':2}
-
>>> x = dict['b']
-
>>> x
-
2
-
>>> x=x+3
-
>>> x
-
5
-
>>> dict
-
{'a': [1, 2, 3, 4, 5], 'b': 2}
-
>>>
那么问题来了,变量赋值传递时什么情况下是传值(拷贝),什么情况下是传址(引用)呢?
1、直接拷贝
当我们不知道是引用还是拷贝的情况下,可以显式的拷贝。比如字典对象本身都具有拷贝的方法:
x=dict.copy()
没有拷贝方法的对象,也是可以拷贝的。这儿我们引入一个深拷贝的概念,深拷贝——即python的copy模块提供的一个deepcopy方法。深拷贝会完全复制原变量相关的所有数据,在内存中生成一套完全一样的内容,在这个过程中我们对这两个变量中的一个进行任意修改都不会影响其他变量。还是上面的代码,如果改成如下:
-
import copy
-
dict = {'a':[1,2,3,4,5],'b':2}
-
x = copy.deepcopy(dict['a'])
-
for i in range(5):
-
x[i] = 0
-
print(dict['a'])
运行结果dict值不受影响。
除了深拷贝,copy模块还提供一个copy方法,称其为浅拷贝,对于简单的对象,深浅拷贝都是一样的,上面的词典对象的copy方法就是浅拷贝。
-
>>> dict
-
{'a': [8, 2, 3, 4, 5], 'b': 4}
-
>>> dd=copy.copy(dict)
-
>>> dd
-
{'a': [8, 2, 3, 4, 5], 'b': 4}
-
>>> dd['a'][0]=7
-
>>> dd
-
{'a': [7, 2, 3, 4, 5], 'b': 4}
-
>>> dict
-
{'a': [7, 2, 3, 4, 5], 'b': 4}
-
>>> ee=dict.copy()
-
>>> ee
-
{'a': [7, 2, 3, 4, 5], 'b': 4}
-
>>> ee['a'][0]=9
-
>>> ee
-
{'a': [9, 2, 3, 4, 5], 'b': 4}
-
>>> dict
-
{'a': [9, 2, 3, 4, 5], 'b': 4}
-
>>> ee['b']=5
-
>>> ee
-
{'a': [9, 2, 3, 4, 5], 'b': 5}
-
>>> dict
-
{'a': [9, 2, 3, 4, 5], 'b': 4}
-
>>>
浅拷贝时改变第一层次相互不受影响(上例中词典b值的修改),第二层次(上例中词典a的列表值修改)就相互影响了,改一个,其他跟着变。看看id吧:
-
>>> id(dict)
-
20109472
-
>>> id(dd)
-
20244496
-
>>> id(ee)
-
20495072
-
>>> id(dd['a'])
-
20272112
-
>>> id(ee['a'])
-
20272112
-
>>> id(dict['a'])
-
20272112
-
>>>
可见词典各个拷贝的id是不同的,但词典a值的id是相同的。如果我们需要真正意义的拷贝,就用深拷贝吧。
2、传递规则
Python赋值过程中不明确区分拷贝和引用,一般对静态变量的传递为拷贝,对动态变量的传递为引用。(注,对静态变量首次传递时也是引用,当需要修改静态变量时,因为静态变量不能改变,所以需要生成一个新的空间存储数据)。
- 字符串,数值,元组均为静态变量
- 列表,字典为动态变量。
变量有时比较复杂,存在组合现象,比如字典中包含列表,列表中包含字典,但赋值时,总是属于某个类型。如果实在不清楚状况,可以试验一下,用id()这个函数看看,如果是引用,两个变量指向的地址是相同的。例如:
-
>>> a=6
-
>>> id(a)
-
10413476
-
>>> b=a
-
>>> id(b)
-
10413476
-
>>> b=8
-
>>> id(b)
-
10413452
-
>>>
修改变量b之前,a和b指向的地址是相同的,修改b后,地址就变了。
值传递:方法调用时,实际参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值。
>>> a1 = 520
>>> a2 = a1
>>> print a1
520
>>> print a2
520
>>> a2 = a1 + 1
>>> print a2
521
>>> print a1
520
>>>
引用传递:也称地址传递,在方法调用时,实际上是把参数的引用(传的是地址,而不是参数的值)传递给方法中对应的形式参数,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。
>>> a1 = [1,2]
>>> a2 = a1
>>> a2.append(3)
>>> print a1
[1, 2, 3]
>>> print a2
[1, 2, 3]
>>>
在Python中,数字、字符或者元组等不可变对象类型都属于值传递,而字典dict或者列表list等可变对象类型属于引用传递。
如果要想修改新赋值后原对象不变,则需要用到python的copy模块,即对象拷贝。对象拷贝又包含浅拷贝和深拷贝。下面用例子来说明
import copy
l1 = [[1, 2], 3]
l2 = copy.copy(l1)
l3 = copy.deepcopy(l1)
l2.append(4)
l2[0].append(5)
l3[0].append(6)
最后的结果:
l1 = [[1, 2, 5], 3]
l2 = [[1, 2, 5], 3, 4]
l3 = [[1, 2, 6], 3]
从上例可以看出,copy.copy属于浅拷贝,拷贝的是第一层list,而copy.deepcopy属于深拷贝,对list所有子元素都进行深拷贝。