在之前的文章 http://www.cnblogs.com/bitpeng/p/4748148.html 中,大家看到了ret.append(path) 和ret.append(path[:])的巨大差别。这和Python的对象机制有关。现在谈谈这个问题!
我们知道,Python有可变对象和不可变对象,他们的表现行为也迥然不同。先来几个简单的问题:
1 def foo1(arg): 2 arg = 5 3 print(arg) 4 5 x = 1 6 foo(x) # 输出5 7 print(x) # 输出1
1 def foo2(arg): 2 arg.append(3) 3 4 x = [1, 2] 5 print(x) # 输出[1, 2] 6 foo(x) 7 print(x) # 输出[1, 2, 3]
1 def foo3(arg): 2 arg = [3] 3 4 x = [1, 2] 5 print(x) # 输出[1, 2] 6 foo(x) 7 print(x) # 输出[1, 2]
一、Python参数问题
1.关于Python默认参数问题,我之前一篇博文有过描述。请参考这里:http://www.cnblogs.com/bitpeng/p/4747765.html
2.关于Python的参数传递问题。原来接触过C/C++ 的朋友,肯定想过,Python函数调用时,到底是值传递,还是引用传递。看到网上说的最多的是:对于不可变对象,是值传递;对于可变对象,是引用传递。可是我个人感觉,这个描述不是很准确,因为Python函数调用时,不管是可变对象,还是不可变对象,参数引用的都是实参。但是,既然为什么都是引用,结果却表现不同,这就和Python的对象有关。
在Python中,任何东西都是对象。
Python使用对象模型来储存数据,任何类型的值都是一个对象。所有的python对象都有3个特征:身份、类型和值。
身份:每一个对象都有自己的唯一的标识,可以使用内建函数id()来得到它。这个值可以被认为是该对象的内存地址。
类型:对象的类型决定了该对象可以保存的什么类型的值,可以进行什么操作,以及遵循什么样的规则。type()函数来查看python 对象的类型。
值:对象表示的数据项。
运算符is
、is not
就是通过id()的返回值(即身份)来判定的,也就是看它们是不是同一个对象的“标签”。
这里有个很形象的例子:http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables
根据这里,我们可以知道,Python是通过名字来访问对象。这和其他语言有很大的不同,比如在C中,你定义了变量(注意是定义不是声明),那么编译后就一定会给变量分配内存,以后对该变量的读写就是通过该内存地址进行的。而Python中,只会给对象分配内存,然后再通过名称来访问该对象而已。所以,这就是为什么Python名称可以赋值给任何类型的原因(并不是真的赋值). 参数传递也一样,只是用形参名称来访问实参所表示的对象。跟所谓的值传递、引用传递没有任何关系。所以:在foo2()中,append()执行列表方法,当然也会影响实参;而赋值操作,只是将x绑定到另一个列表对象。这样,原来的实参列表还是原来的,没有变化,现在foo2()和foo3()应该能懂了吧。
关于Python参数调用的结论:
- Python函数不会替换调用参数所引用的对象。
- 对一个参数名重新赋值不会起任何作用。
- Python函数可以修改参数,如果这个参数是可变的。
- 在Python中没有什么是被隐式复制的。
- 在Python中函数调用时,不存在什么所谓的值传递和引用传递,只是通过名称(形参)来访问对象(实参所代表的对象), 这和Python对象机制是一致的!
二、可变对象,不可变对象复制行为
1 >>> a = 1 2 >>> b = 1 3 >>> a is b 4 True 5 >>> import copy 6 >>> c = copy.deepcopy(a) 7 >>> c 8 1 9 >>> c is a 10 True 11 >>> s = "abc" 12 >>> c = copy.deepcopy(s) 13 >>> c 14 ‘abc‘ 15 >>> c is s 16 True
>>> a = "abc"; b = a; c = a[:]; d = copy.deepcopy(a) >>> a,b,c,d (‘abc‘, ‘abc‘, ‘abc‘, ‘abc‘) >>> a is b ;c is a; d is a True True True
看到了吗?对于字符、字符串、数值型,不管是赋值,切片,还是深度复制,他们都是同一个对象。但是对于元组呢?表现有所不同
1 >>> a = (1,2,[3,4]); b = a; c = a[:];d = copy.deepcopy(a) 2 >>> b is a; c is a; d is a 3 True 4 True 5 False 6 >>> a[2][0] = 0;a,b,c,d 7 ((1, 2, [0, 4]), (1, 2, [0, 4]), (1, 2, [0, 4]), (1, 2, [3, 4]))
显然,对于元组并且包含可变元素时,切片和深度复制表现时不一样的。
结论:
1、赋值:简单地拷贝对象的引用,两个对象的id相同。
2、浅拷贝:创建一个新的组合对象,这个新对象与原对象共享内存中的子对象。
3、深拷贝:创建一个新的组合对象,同时递归地拷贝所有子对象,新的组合对象与原对象没有任何关联。虽然实际上会共享不可变的子对象,但不影响它们的相互独立性。
浅拷贝和深拷贝的不同仅仅是对组合对象来说,所谓的组合对象就是包含了其它对象的对象,如列表,类实例。而对于数字、字符串以及其它“原子”类型,没有拷贝一说,产生的都是原对象的引用。
可能有些朋友会有疑问,元组时不可变的,为什么还可以给a[2][0]赋值呢。
>>> a = (1,2,[3,4])>>> a[2] = [1,2,3] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: ‘tuple‘ object does not support item assignment >>> a[2][:] = [1,2,3] >>> a (1, 2, [1, 2, 3])
这里我们这样认为,元组可以包含可变对象,只要元组的每个元素的id没有变化即可。所以a[2] = [1,2,3]是尝试把列表第3个元素引用其他的列表,id肯定变了;所以不支持。但是a[2][:] = [1,2,3]是原地赋值,虽然列表本身变了,但是列表本身的id号没变,所以支持。
结论:对于不可变对象如元组:仅仅代表,他的每一个元素的id号是不可变的。如果元组本身包含可变元素,那么还是可以改变他的值的!
三、Python与二维数组
之前做算法题,需要用二维数组,所以很当然的想到了二维列表。
需求:初始化一个8行8列的数组,每个元素初始化为0.
当时,我想当然的是这样做的:
>>> a = [[0] * 8 ]* 8; a [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
当时还想着,看,多完美!!!
结果程序总是死循环,不能正常退出。刚开始,结果对程序逻辑进行一次次的检查后,问题还是无解!于是开始调试。最后发现了诡异的问题。
问:a[i][j] = 1 后,程序会发送什么, 其中0 <= i, j < 8.
>>> a = [[0] * 8 ]* 8; a
[[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
>>> a[0][0] = 1;a
[[1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0]]
呵呵,发现了吗?给a[0][0]赋值后,结果很多其他的元素也都变了。现在看这个问题其实很简单,因为列表执行乘法,相当于是浅复制。所以a中看似有8个列表,实际上引用的都是同一个。ps:这个问题,实际上书上讲过,我忘了!结果写程序时,出了这样的问题.
>>> for i in a: ... print id(a) ... 140383550710200 140383550710200 140383550710200 140383550710200 140383550710200 140383550710200 140383550710200 140383550710200
也许有人会问,那为什么a = [[0] * 8 ]* 8; a[0][0] = 1后, 只有每一列第一个元素变为1,而其他的不变呢?这个问题也简单:因为0是不可变对象。实际上,a = [[0] * 8 ]* 8执行完毕后,a的所有元素id都是相同的。但是,执行a[0][0] = 1后,a[0][0] 的id号已经变了。
>>> for i in a[0]:print id(i) ... 32276848 32276848 32276848 32276848 32276848 32276848 32276848 32276848
>>> a[0][0] = 1 >>> for i in a[0]:print id(i) ... 32276824 32276848 32276848 32276848 32276848 32276848 32276848 32276848
结论:需要二维数组时,老老实实的用列表推导。
>>> a = [[0 for i in range(8)] for i in range(8)] >>> a [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]] >>> a[0][0] = 1 >>> a [[1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
现在已经一切正常了。