python对象引用和垃圾回收

Posted Sakura

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python对象引用和垃圾回收相关的知识,希望对你有一定的参考价值。

变量="标签"

变量a和变量b引用同一个列表:

>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]

使用"标签"很形象的解释了变量    =========>   列表[1, 2, 3]是一个物品,而a和b都是给这个物品贴上的标签。因此,改变a的内容,b的内容也改变了。

"is"和"=="

有一个人叫做李华,1997年生,身体情况工作信息记录为info,有个小名叫"小华"。

>>> lihua = {name:lihua,born:1997,information:info}
>>> xiaohua = lihua
>>> xiaohua is lihua
True
>>> id(xiaohua),id(lihua)
(2072419437304, 2072419437304)
>>> xiaohua[information] = new_info
>>> lihua
{name: lihua, born: 1997, information: new_info}

可见xiaohua和lihua指代同一个对象,假如有个冒充者(李华)说他是李华,身份信息一模一样,记为anony。

>>> anony = {name: lihua, born: 1997, information: new_info}
>>> anony == lihua
True
>>> anony is lihua
False

此时使用"is"和"=="判断结果是不同的。lihua和xiaohua绑定同一个对象,xiaohua是lihua的别名;而lihua和anony绑定不同对象。

"=="比较的是对象的值,而"is"比较对象的标识。

在Python中,对象的标识就是id()函数返回值,而is比较的就是这个返回值的整数表示。在Cpython中,id()返回的是对象的内存地址,在其他Python解释器中可能是别的值。最主要的是,id()函数返回值在对象的生命周期中一定不会改变

写程序是一般关注值,因此==出现频率较高,而在变量和单例值之间比较时应该使用is。除此之外,is运算符比==快,因为它不能重载,解释器不需要寻找并调用特殊方法,直接比较整数id;a==b是语法糖,等同于a.__eq__(b),继承自object的__eq__方法比较两个对象的id,结果与is一样,而覆盖__eq__方法后结果可能就与is结果不同了。

元组是"可变的"

元组保存对象的引用,如果引用的元素是可变的,即使元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指tuple数据结构的物理内容(即引用)不可变,与引用的对象无关

t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40])
print(t1 == t2)
True

print(id(t1[-1]))
3031106993224                  #标识

t1[-1].append(50)
print(t1)
(1, 2, [30, 40, 50])              #修改t1[-1]列表

print(id(t1[-1]))
3031106993224                   #标识没变

print(t1 == t2)
False                                    #值改变了,不相等

浅复制与深复制

默认作浅复制

复制列表最简单的方式是使用内置类型的构造方法,以list为例:

l1 = [3, [40, 50], (6, 7, 8)]
l2 = list(l1)
print(l2)
[3, [40, 50], (6, 7, 8)]

print(l2 == l1)
True                    #副本与原副本相等

print(l2 is l1)
False                   #副本与原副本指代不同的对象

当然,可变序列都可以用 [:] 来复制,而无论是构造方法还是 [:] 复制都是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用),如果元素是可变的,就会出现问题。

l1 = [3, [40, 50], (6, 7, 8)]
l2 = list(l1)
l1.append(99)
l1[1].remove(50)
print(l1:, l1)
print(l2:, l2)
l2[1] += [22, 33]
l2[2] += (9, 10)
print(l1:, l1)
print(l2:, l2)


#结果
l1: [3, [40], (6, 7, 8), 99]           
l2: [3, [40], (6, 7, 8)]                #对比l1,由于浅复制,追加99对l2无影响,而对元组l1里面的可变对象[40, 50]执行删除操作却影响到了l1,说明l2和l1绑定同一个列表
l1: [3, [40, 22, 33], (6, 7, 8), 99]
l2: [3, [40, 22, 33], (6, 7, 8, 9, 10)]   #+=操作就地修改列表,因此l2与l1同时被修改,而+=对于元组这种不可变对象来说,会重新创建一个元组,重新绑定给l2,修改后,l2中的那个元组与l1中的不是同一个


print(id(l1[2]))
print(id(l2[2]))

print(id(l1[2]))
print(id(l2[2]))

#替换为打印id,发现l2的元组id最后改变了
2377750817600
2377750817600
2377750817600
2377749721512

浅复制容易,但有时会出现不想要也很意外的结果,就需要深复制。

为任意对象作浅复制和深复制

copy模块提供copy用于浅复制和deepcopy用于深复制(副本不共享内部对象的引用)。

定义一个类bus表示校车,有乘客上车下车方法:

import copy


class Bus:

    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)

    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)


bus1 = Bus([Alice, Bob, David])   #原校车
bus2 = copy.copy(bus1)            #copy方法复制的校车
bus3 = copy.deepcopy(bus1)        #deepcopy方法复制的校车

print(id(bus1), id(bus2), id(bus3))
bus1.drop(David)            #bus1的David下车
print(bus2.passengers)

print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers))
print(bus3.passengers)

#结果
2765338083960 2765338084072 2765338083456   #三个不同的Bus对象
[Alice, Bob]                      #bus2的David消失了
2765338349448 2765338349448 2765337978376   #bus1,bus2共享同一个列表对象,bus3则有另一个列表
[Alice, Bob, David]             #bus3没有改变

一般来说,深复制不是一件简单的事情。如果对象有循环引用,那么这个朴素算法会进入无限循环。deepcopy函数会记住已经复制的对象,因此能优雅的处理循环引用:

from copy import deepcopy

a = [10, 20]
b = [a , 30]
a.append(b)
print(a)
c = deepcopy(a)
print(c)


[10, 20, [[...], 30]]
[10, 20, [[...], 30]]

深复制有时处理得太深,对象可能会引用不该复制的外部资源或单例值,此时可以实现特殊方法__copy__()和__deepcopy__(),控制copy和deepcopy的行为。

 函数的参数作为引用

python唯一支持的方式是共享传参。类似于java的引用传参。它是指函数各个形式参数获得实参中各个引用的副本,即函数内部形参是实参的别名

这样,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象标识(即不能把一个对象替换为另一个对象)

不要使用可变类型作为参数默认值

可选参数可以有默认值,但应该避免使用可变对象作为参数默认值。如果使用可变参数,后果见例子:

定义一辆校车,passenger默认值不用None而用[ ]

class Bus:

    def __init__(self, passengers=[]):
        self.passengers = passengers

    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)


bus1 = Bus([Alice, Bob])      #1车原两人
print(bus1.passengers)
bus1.pick(Jane)
bus1.drop(Alice)
print(bus1.passengers)             #上一人下一人

bus2 = Bus()                         #2车空车
bus2.pick(David)
print(bus2.passengers)           #上一人

bus3 = Bus()                          #3车空车
bus3.pick(Mike)
print(bus2.passengers)            #上一人

print(bus2.passengers is bus3.passengers)
print(bus1.passengers)

奇怪的现象出现了:

[Alice, Bob]
[Bob, Jane]
[David]
[David, Mike]
True
[Bob, Jane]

1车正常行驶,3车出现”幽灵学生“,上二车的David出现在了3车。事实上,可看到bus2和bus3引用的是同一个乘客列表。

实例化Bus时,如果传入乘客可以正常运作,但是不为Bus指定乘客的话,奇怪的事情发生,这是因为self.passengers变成了passengers参数默认值的别名。默认值在定义函数时计算,因而默认值变为了函数对象的属性,如果默认值是可变对象,那么后续函数调用都会受到影响。审查Bus.__init__对象

print(Bus.__init__.__defaults__)
#‘David‘, ‘Mike‘成为默认乘客
([David, Mike],)

所以,如果定义的函数接受可变参数,应该慎重考虑调用方是否期望修改传入的参数。例如校车写成深复制那一节的样子。

del和垃圾回收

del语句删除名称,或者说删除标签。(删除一个物品的标签,而不是删除这个物品)del命令可能导致对象被当作垃圾回收,即满足下列条件之一时:

1.删除的变量保存的是对象的最后一个引用

2.无法得到对象

重新绑定也可能会导致对象的引用数量归零,导致对象销毁。

python采用引用计数算法来进行垃圾回收,每个对象都会统计有多少个引用指向自己,当引用计数器归零时,对象就立即销毁。python2.0采用分代垃圾回收算法,用于处理循环引用。

见下例:

import weakref

s1 = {1, 2, 3}
s2 = s1

def bye():
    print(bye)

ender = weakref.finalize(s1, bye)   #注册一个回调函数,在{1,2,3}销毁时使用
print(ender.alive)
del s1
print(ender.alive)
s2 = helloworld
print(ender.alive)

#结果发现del s1后,对象仍然存活,而s2重新绑定了对象,于是无法获取对象,导致对象被销毁
True
True
bye
False

弱引用

有引用时对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。

弱引用不会增加对象引用数量,引用的目标对象称为所指对象,因此,弱引用不会妨碍所指对象被当作垃圾回收(任何无引用的时候)。弱引用在缓存中很有用,因为我们不想因为被缓存引用着而始终保存缓存对象。

python提供weakref模块来控制弱引用。

weakref.ref

import weakref
import sys

set1 = {1, 2}
print(sys.getrefcount(set1))         #打印引用计数
wref = weakref.ref(set1)             #创建弱引用
print(wref)                          #打印弱引用 
print(sys.getrefcount(wref))
set2 = wref()                         #!!!弱引用时可调用对象,返回的是被引用的对象,若所指对象不存在则返回None 
print(set2 is set1)
print(sys.getrefcount(set1))          
set1 = None
set2 = None
print(wref)  

结果:

2
<weakref at 0x0000024BADFA0408; to set at 0x0000024BADEE99E8>
2
True
3            #调用弱引用返回被引用对象绑定到set2,所以引用显示为3                                           
<weakref at 0x0000024BADFA0408; dead>      #弱引用失效

初始引用为2的原因是:当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用

weakref.WeakValueDictionary

WeakValueDictionary类实现一种可变映射,里面的值是对象的弱引用,被引用对象在程序其他地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary中删除。

import weakref

class Cheese:
    def __init__(self, kind):
        self.kind = kind

    def __repr__(self):
        return Cheese(%r) % self.kind

stock = weakref.WeakValueDictionary()
catalog = [Cheese(A), Cheese(B), Cheese(C), Cheese(D), Cheese(E), Cheese(A)]
for cheese in catalog:
    stock[cheese.kind] = cheese

print(sorted(stock.keys()))
del catalog
print(sorted(stock.keys()))
del cheese
print(sorted(stock.keys()))

结果:

[A, B, C, D, E]
[A]
[]

删除引用后[‘A‘]奶酪还在,是因为临时变量引用了对象,这可能导致该变量存在的时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时会被销毁。示例中是全局变量,需显式删除才会消失。

 

Weak模块还有proxy,WeakSet,WeakKeyDictionary等

 

//proxy(obj[,callback])函数来创建代理对象。使用代理对象就如同使用对象本身一样,而不需要像ref那样显示调用

//WeakKeyDictionary的键是弱引用,它的实例可以为应用中其他部分拥有的对象附加元数据,这样就无需为对象添加属性

//WeakSet类保存元素弱引用的集合类,元素没有强引用时,集合会把它删除

以上来自《流畅的python》第8章

以上是关于python对象引用和垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章

python垃圾回收

Python 垃圾回收

Python 中的垃圾回收机制

python对象引用和垃圾回收

Python垃圾回收机制

**Python垃圾回收机制