Python对象引用、可变性和垃圾回收
1. Python中的变量是什么
Python和Java中的变量不一样,Python的变量实质上是一个指针
例如:a = 1
a = "abc"
可以理解为先生成1或者abc,然后再把a指向这个对象,a本身没有类型,这也即是Python不需要声明类型的原理。
也可以形象的将python中的变量看做一个便利贴,是指给已生成的对象进行贴上标记而已。
a = [1,2,3]
b = a
print(id(a), id(b))
b.append(4)
print(a)
>>> 39658440 36958440
>>> 1 2 3 4
# 这里a和b的地址一样,两张便利贴贴在同一个对象上。
# 操作b所指向的对象之后,a指向的对象也发生了变化。
2. == 与 is 的区别
a和b指向不同的对象,它们的id也不想等,但是Python有一个intern机制,对于小整数、小字符等会临时分配同样的地址,其本质是一种优化。
a == b之所以成立的原因是,在list内置一个eq的方法,会默认取出对象中的值进行对比。
a = [1,2,3,4]
b = [1,2,3,4]
print(a == b)
print(a is b)
print(id(a), id(b))
>>> True
>>> False
>>> 39724040 39724616
3. 垃圾回收机制和del语句
Python中的垃圾回收是以“引用计数”为主,“标记清除”和“分代回收”为辅。
【引用计数】
Python默认的垃圾收集机制是“引用计数”。每个对象维护了一个ob_ref引用计数字段,当新的引用指向该对象时,引用计数加1,当一个对象的引用被销毁时减1。一旦对象的引用计数为0,该对象立即被回收,所占用的内存将被释放。
实例:
a = object()
b = a
del a
print(b)
print(a)
>>> <object object at 0x00000000003D90F0>
>>> NameError:name 'a' is not defind
# 当a,b都指向object()时,object()的计数为2,删除a时,计数为1。当计数为0时,其内存就会被释放。
class A:
def __del__(self):
pass
# 类中会有一个del方法,一般在垃圾回收时被调用
【标记清除】
标记——清除(Mark——Sweep)是一种基于追踪回收技术实现的垃圾回收算法。对象之间通过引用指针连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象出发,沿着有向边遍历对象,可达的对象标记为有用的对象,不可达的对象就是要被清除的对象。标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。
【分代回收】
Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率随着对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾回收机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。
4. 一个经典的参数错误
def add(a, b):
a += b
return a
# 为方便演示,以下为伪代码
if __name__ == "__main__":
a = 1
b = 2
c = add(a, b)
print(c)
>>> 3
>>> 1 2
a = [1,2]
b = [3,4]
c = add(a, b)
print(c)
>>> [1,2]
>>> [1,2,3,4] [3,4]
a = (1, 2)
b = (1, 2)
c = add(a, b)
print(c)
>>> (1, 2)
>>> (1, 2) (3, 4)
出现以上情况是因为list是可变对象,容易被修改。