终于写完了这篇文章~
之前总觉得这里牵扯范围太多,一堆概念搅在一起,不好梳理,但这次借助之前留下的“可变对象、不可变对象”成功地把这一串都清了出来~
这次主要还是想讲引用的问题,但是各种前置概念比较多,大家就顺着看下去吧~
本篇文章涉及内容:
可变对象、不可变对象
python赋值逻辑: 动态类型
Python引用传递
深拷贝,浅拷贝
可变对象、不可变对象
这个概念可能平时不会去注意,但是不搞清楚的话,真的会对很多细节上的代码逻辑一脸懵逼
可变对象(mutable object): 对象创建后可以被改变,并且不需要重新开辟内存
不可变对象(immutable object): 对象被创建后不可改变,如要非要“改变”则只能重新开辟内存,创建新的对象
这里给个文档,是 MutableSequence, MutableSet, MutableMapping:
https://docs.python.org/2/library/collections.html#collections.MutableSequence
这个文档是collections.abc里的,abc是 Abstract Base Classes,是很多内置容器的抽象基类,同时也可以提供mix-in使用方式
常见的不可变对象: int, float, long, str, unicode, tuple, function
常见的不可变对象: list, dict, set,代码上看就是,一般会实现 , , 等方法
举例来说,str是不可变对象,改变其内容后,其id发生了变化,而list是可变对象,改变其内容后,其id不变
python的赋值逻辑: 动态类型
首先我们知道python里一切皆对象,在做赋值操作时,就是让变量指向对象的内存地址。这里给出三个概念: 变量,对象,引用
对象,就是会占据内存的东西,是有类型的,可以用内置函数type来看
变量,可以理解为在命名空间里注册的一个值,它是没有类型的,纯粹是一个方便我们使用的东西
引用,就是“变量指向对象”这个操作,一个变量只能指向一个对象,但一个对象可以被多个变量指。每个对象会有一个“引用计数器”,用来记录有多少个变量指向了它,当这个数变为0时,此对象会被gc回收,其所占据的内存空间会被释放
动态类型(dynamic typing)是什么意思呢,就是说python里的对象和变量是完全分开的,对象归内存,变量归命名空间,他们之间只会通过引用连接。
我们知道Java里声明变量时必须要指定其类型,python的“动态类型”这个名字的意思就是,变量是不需要“声明类型”的,变量也没有类型。比如一个变量一开始指向str,一会又指向list,一会又指向int,变量所指的对象代表的类型是不固定的,是动态的,这就叫做“动态类型”
对于不可变对象来说, 这个操作分三部分:
创建 这个对象
在各个命名空间里按顺序找 这个变量名,如果没有就在locals里创建
让 指向 ,此时 这个变量的被引用数从0变成1
这里是让a指向 的内存地址,而 等价于 ,也就是新创建了一个对象,内容是 ,然后让a指向这个新的地址(这里a就是一个引用)。这时,对之前的 这个对象来说,没有任何变量指向它,好可怜的,他的引用计数器归零了,立刻就会被python的gc给回收掉了
对于可变对象来说, 这个操作分四部分(顺序是我猜的,没有验证):
创建一个空list,并分配空间
创建 这三个对象,并将其引用加入list,注意加的是引用(这里忽略小数据常量池的影响)
在各个命名空间里按顺序查找 这个变量名,如果没有就在locals里创建
让b指向这个list
它与不可变对象的区别在于,当向list里添加元素的时候,list本身的地址是不变的,只是在list对应对象的内存空间里,多增加了一个新增元素的引用
动态类型,是往下继续理解的基础
python参数传递
先说结论: python的参数传递,全是引用传递,没有所谓的“值传递”
为什么呢?很好理解啊,因为变量存的都是引用,根本没有值,根本谈不上值传递。
那么为什么很多时候看起来很像值传递呢?因为看上去像值传递的都是不可变对象,因为不可以修改,所以表现形式类似于值传递,看代码:
这其实是这种情况: 多个变量指向同一个对象,即一个对象有多个引用
的时候,因为字符串是不可变对象,所以d会指向一个新的对象, 的引用计数器从2变回1,但c和d指向的完全不是同一个对象了
时,是在f对应的对象里,增加字符串 的对象的引用。而f和e指向的都是同一个对象,自然输出e的时候,也会带有对f的修改
调用函数时的参数传递,跟上面是一致的:
我们可以看到,h里的值已经被改变了~
这就是py2实现py3里 字段的重要方式,这个方式虽然不太好看,但真的是py2让外界获取闭包里的信息的常规操作,可以不用,但一定要明白
同时这样也是昨天的
Python内置模块-itertools(开一个新坑)
文章最后提出的几个问题中,第三个问题的答案: 因为对可变对象而言,对其内部数据的修改在所有引用上都可以看到效果,无论命名空间如何
深拷贝,浅拷贝
这里说的深拷贝和浅拷贝,指的是内置 模块的 和 两个函数
给一个别人的图文并茂版,这里还有不同数据类型在内存中的存储方式:
http://www.cnblogs.com/adc8868/p/5647501.html
浅拷贝的作用是,复制传入的变量指向的这个对象。这里需要注意的是,如果这是个可变对象,那么复制时会同时复制这个对象里直接存储的所有引用信息(没错是直接复制了引用,而非实际内容),因为复制的是直接存储的引用,因此这个列表里直接存储的各个对象不会被复制,只是都会增加一次引用次数。如代码,可以看到最外层id变了,但内部都仍然是一样的。
深拷贝的作用是,不但复制当前对象,还会递归进去把里面所有的对象都尝试复制一遍,这也是我们常规意义上理解的“复制”
如下面代码, 可以获取x的被引用次数,但调用这个方法的同时也会引用一次,所以最后的结果里-1就是我们想要的数据。可以看到,浅拷贝后,内部数据的id没变,但列表直接数据的第一层的引用数增加了1,而且更内层引用数没变。深拷贝后则是id全变,引用数也变成了正常的1。符合刚才的理解。
引用传递这件事,到这里也差不多就结束了,后面再有就要看gc相关的内容了,这次内容比较多,有什么地方不清楚的留言或者私信我就好~
来源:方禾黎,给我十万加急赶图的妹妹
领取专属 10元无门槛券
私享最新 技术干货