python 引用和对象理解
http://www.cnblogs.com/ShaunChen/p/5656971.html
今天浏览博客的时候看到这么一句话: python中变量名和对象是分离的;最开始的时候是看到这句话的时候没有反应过来。决定具体搞清楚一下python中变量与对象之间的细节。(其实我感觉应该说 引用和对象分离 更为贴切)
从最开始的变量开始思考:
在python中,如果要使用一个变量,不需要提前进行声明,只需要在用的时候,给这个变量赋值即可 (这个和C语言等静态类型语言不同,和python为动态类型有关)。
举第一个栗子:
a = 1
这是一个简单的赋值语句,整数 1 为一个对象,a 是一个引用,利用赋值语句,引用a指向了对象1;这边形象比喻一下:这个过程就相当于“放风筝”,变量a就是你手里面的“线”,python就跟那根“线”一样,通过引用来接触和拴住天空中的风筝——对象。
你可以通过python的内置函数 id() 来查看对象的身份(identity),这个所谓的身份其实就是 对象 的内存地址:
注:
python一切皆对象的理念,所以函数也是一个对象,因此可以使用 id() 函数的__doc__方法来查看这个函数的具体描述:
|
1
2
|
>>> id.__doc__ "id(object) -> integer\n\nReturn the identity of an object. This is guaranteed to be unique among\nsimultaneously existing objects. (Hint: it's the object's memory address.)" |
第二个栗子:
a = 2
a = 'banana'
利用上面第一个栗子用到的 id()函数:
|
1
2
3
4
5
6
|
>>> a = 1>>> id(a)24834392>>> a = 'banana'>>> id(a)139990659655312 |
第一个语句中, 2是储存在内存中的一个整数对象,通过赋值 引用a 指向了 对象 1
第二个语句中,内存中建立了一个字符串对象‘banana’,通过赋值 将 引用a 指向了 ‘banana’,同时,对象1不在有引用指向它,它会被python的内存处理机制给当我垃圾回收,释放内存。
第三个栗子:
a = 3
b = 3
通过函数查看 变量a 和 变量b的引用情况:
|
1
2
3
4
5
6
|
>>> a = 3>>> b = 3>>> id(a)10289448>>> id(b)10289448 |
在这里可以看到 这俩个引用 指向了同一个 对象,这是为什么呢? 这个跟python的内存机制有关系,因为对于语言来说,频繁的进行对象的销毁和建立,特别浪费性能。所以在Python中,整数和短小的字符,Python都会缓存这些对象,以便重复使用。
第四个栗子:
1. a = 4
2. b = a(这里就是让引用b指向引用a指向的那个对象)
3. a = a + 2
通过函数查看引用情况:
当执行到第2步的时候,查看一下 a 和 b 的引用:
|
1
2
3
4
5
6
|
>>> a = 4>>> b = a>>> id(a)36151568>>> id(b)36151568 |
可以看到 a 和 b 都指向了 整数对象 4
接下来指向第3步:
|
1
2
3
4
5
|
>>> a = a+2>>> id(a)36151520>>> id(b)36151568 |
可以看到 a 的引用改变了,但是 b 的引用未发生改变;a,b指向不同的对象; 第3句对 a 进行了重新赋值,让它指向了新的 对象6;即使是多个引用指向同一个对象,如果一个引用值发生变化,那么实际上是让这个引用指向一个新的引用,并不影响其他的引用的指向。从效果上看,就是各个引用各自独立,互不影响。
第五个栗子(这个栗子会涉及到 python中的 可变数据类型 和 不可变数据类型):
开始这个栗子之前,请记得注意到 第四个栗子的不同之处。
1. L1 = [1, 2, 3]
2. L2 = L1
3. L1[0] = 10
通过函数查看引用情况:
当执行第1步 和 第二步 的时候,查看一下 L1 和 L2 的引用情况:
|
1
2
3
4
5
6
|
>>> L1 = [1,2,3]>>> L2 = L1>>> id(L1)139643051219496>>> id(L2)139643051219496 |
此时 L1 和 L2 的引用相同,都是指向 [1,2,3]这个列表对象。
接下来,继续执行第3步:
|
1
2
3
4
5
6
7
|
>>> L1[0] = 10>>> id(L1)139643051219496>>> id(L2)139643051219496>>> L2[10, 2, 3] |
同样的跟第四个栗子那样,修改了其中一个对象的值,但是可以发现 结果 并不与 第四个栗子那样, 在本次实验中,L1 和 L2 的引用没有发生任何变化,但是 列表对象[1,2,3] 的值 变成了 [10,2,3](列表对象改变了)
在该情况下,我们不再对L1这一引用赋值,而是对L1所指向的表的元素赋值。结果是,L2也同时发生变化。
原因何在呢?因为L1,L2的指向没有发生变化,依然指向那个表。表实际上是包含了多个引用的对象(每个引用是一个元素,比如L1[0],L1[1]..., 每个引用指向一个对象,比如1,2,3), 。而L1[0] = 10这一赋值操作,并不是改变L1的指向,而是对L1[0], 也就是表对象的一部份(一个元素),进行操作,所以所有指向该对象的引用都受到影响。
(与之形成对比的是,我们之前的赋值操作都没有对对象自身发生作用,只是改变引用指向。)
列表可以通过引用其元素,改变对象自身(in-place change)。这种对象类型,称为可变数据对象(mutable object),词典也是这样的数据类型。
而像之前的数字和字符串,不能改变对象本身,只能改变引用的指向,称为不可变数据对象(immutable object)。
我们之前学的元组(tuple),尽管可以调用引用元素,但不可以赋值,因此不能改变对象自身,所以也算是immutable object.
is关键字:
当然,我们也可以要想知道是否指向同一个对象,我们可以使用 python的 is 关键词,is用于判断两个引用所指的对象是否相同。
就像上述第四个栗子 当进行到 第1步 和 第2步 的时候:
|
1
2
3
4
|
>>> a = 4 ……id(a) = 36151568>>> b =a ……id(b) = 36151568>>> a is b True |
当进行到第3步的时候:
|
1
2
3
|
>>> a = a + 2 ……id(a) = 36151520>>> a is b ……id(b) = 36151568False |
突然想到,对于python 的 深拷贝 和 浅拷贝 的理解,也是可以根据这个进行验证,可以通过第五个栗子进行辅助理解。
python 中 深拷贝和浅拷贝的理解
在总结 python 对象和引用的时候,想到其实 对于python的深拷贝和浅拷贝也可以很好对其的进行理解。
在python中,对象的赋值的其实就是对象的引用。也就是说,当创建一个对象,然后赋给另外一个变量之后,实际上只是拷贝了这个对象的引用。
我们先用 利用切片操作和工厂方法list方法 来阐述一下浅拷贝。
举个栗子:
|
1
2
3
|
Tom = ['Tom', ['age', 10]]Jack = Tom[:] ……切片操作June = list(Tom) |
接下来查看一下 上述三个变量的引用:
|
1
2
3
4
5
6
|
>>> id(Tom)140466700697824>>> id(Jack)140466700700488>>> id(June)140466700770192 |
可以发现,三个变量分别指向了不同的对象。我们再来看一下这三个对象的内容:
|
1
2
3
4
5
6
|
>>> Tom['Tom', ['age', 10]]>>> Jack['Tom', ['age', 10]]>>> June['Tom', ['age', 10]] |
显然这三个对象的内容会是一样的,因为通过上面的 切片操作 以及 工厂函数list 对Tom引用的对象进行了拷贝。接下来再进行进一步验证:
我们对 Jack 和 June 引用的对象进行修改:
|
1
2
3
4
5
6
|
>>> Jack[0] = 'Jack'>>> Jack['Jack', ['age', 10]]>>> June[0] = 'June'>>> June['June', ['age', 10]] |
现在我们打算对Jack的年龄进行修改:
|
1
2
3
|
>>> Jack[1][1] = 20>>> Jack['Jack', ['age', 20]] |
可以看到Jack年龄变为了20; 让我们接下来看一下Tom, June的年龄:
|
1
2
|
>>> print Tom, Jack, June['Tom', ['age', 20]] ['Jack', ['age', 20]] ['June', ['age', 20]] |
奇怪的事情发生了,我们仅仅是修改了 Jack的年龄, 但是Tom 和 June 的年龄跟着改变了, 这是为什么呢?
这个就涉及到了python中浅拷贝的奥秘:
我们先来看一下上面 Tom, Jack, June中内部元素的 内存地址:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
>>> for x in Tom:... print id(x)...140704715293600 --> 'Tom'140704715147816 --> ['age', 20]>>> for x in Jack:... print id(x)...140704715286256 --> 'Jack'140704715147816 --> ['age', 20]>>> for x in June:... print id(x)...140704715286352 -->'June'140704715147816 -->['age', 20] |
仔细观察可以看到,Tom, Jack, June 三个变量的 岁数元素['age', 20] 指向同一个 对象; 那为什么他们的 名字元素 分别指向不同的对象。这是因为,在python中的分为 可变数据对象(列表,字典) 和 不可变数据对象(整型,字符串,浮点型,元祖)。 正是因为这个原因,他们的 名字元素 为字符串,为不可变数据对象,因此开始为 Jack 和 June 重新命名的时候,实际上内存中已经创建了 Jack 和 June对象。而 岁数元素 是 可变数据对象,所以并不会在内存中创建新的对象,Tom,Jack,June的岁数元素都引用同一个对象,导致修改其中一个会让另外俩个的年龄跟着变化。
这个就是python的浅拷贝,其仅仅是拷贝了 一个整体的对象(应该说一个对象最外面的那一层),而对于对象里面包含的元素不会进行拷贝。
接下来,我们 利用copy中的deepcopy方法 来阐述一下 深拷贝:
还是用上面那个栗子:
为了让 Tom, Jack, June之间互不影响,我们用deepcopy方法对Tom进行拷贝生成 Jack 和 June:
|
1
2
3
4
5
6
7
8
9
10
|
>>> Tom = ['Tom', ['age', 10]]>>> import copy>>> Jack = copy.deepcopy(Tom)>>> June = copy.deepcopy(Tom)>>> Jack['Tom', ['age', 10]]>>> June['Tom', ['age', 10]]>>> Tom['Tom', ['age', 10]] |
让我们看一下Tom, Jack, June分别指向的内存地址:
|
1
2
|
>>> print id(Tom), id(June), id(Jack)140707738759392 140707738799280 140707738797192 |
三个内存地址不同,然后我们接着改变Jack 和 June的名字,并查看修改后它们的内部元素所指向的内存地址:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
>>> Jack[0] = 'Jack'>>> June[0] = 'June'>>> Tom['Tom', ['age', 10]]>>> Jack['Jack', ['age', 10]]>>> June['June', ['age', 10]]>>> for x in Tom:... print id(x)...140707738882976 --> 'Tom'140707738737192 --> ['age', 10]>>> for x in Jack:... print id(x)...140707738875584 --> 'Jack'140707738910016 --> ['age', 10]>>> for x in June:... print id(x)...140707738876640 -->'June'140707738910160 --> ['age', 10] |
可以清楚的看到,他们的内部元素也指向了不同的对象,说明通过deepcopy方法,对元素进行了彻底的拷贝(包括内部元素)。
最后总结一下思路:
思路一:利用切片操作和工厂方法list方法拷贝就叫浅拷贝,只是拷贝了最外围的对象本身,内部的元素都只是拷贝了一个引用而已。
思路二:利用copy中的deepcopy方法进行拷贝就叫做深拷贝,外围和内部元素都进行了拷贝对象本身,而不是引用。
但是对于数字,字符串和其他原子类型对象等,没有被拷贝的说法,即便是用深拷贝,查看id的话也是一样的,如果对其重新赋值,也只是新创建一个对象,替换掉旧的而已。
浙公网安备 33010602011771号