深入弱引用与终结器

前言

在之前讲解Python底层变量模型时,我们提到CPython解释器中的变量是依靠四层双向循环链表进行维护的,GC是以引用计数为主,分代回收优化下的标记清除为辅的技术实现的,而且被确定为要消除的变量并不会立刻释放占用的内存,而且转入对应类型的free_list进行缓存,从而节省了申请malloc,free服务的额外开支。

很多时候,我们或许对变量释放机制有着更加细粒度的需求。我们都知道del方法只会删除栈空间中的引用变量,而不会删除堆空间上的变量,那么有什么方法可以让我们删除某个确定的对象(比如首次创建这个对象时传递给的引用变量)时,真正直接将它指向的对象也删除吗?

举个例子:

>>>a=[1,2,3]>>>b=a

引用模型如下:

a是首次创建时出现的引用对象,b是被a传递的,可能是别的函数借过去用的。我们现在希望删除变量a可以直接把[1,2,3]从堆空间上去除。

在不考虑循环引用的前提下,想要真正删除一个变量指向的堆空间对象,那就必须让这个对象的引用计数器归零,最直接的方法就是把所有引用一一删除,但是如果我们的引用有很多,比如有a,b,c,d,....一堆变量都指向了同一个对空间变量,那么一个个把引用全都删除显得非常不优雅,也不可靠,还容易漏。

有的朋友可能就会说了,那么我找一个容器把这些引用全部装起来不就行了吗?比如这样:

>>>variable=[None,None]>>>variable[0]=[1,2,3]>>>variable[1]=var>>>variable[1]=variable[0]>>>variable[[1,2,3],[1,2,3]]>>>delvariable

这样看起来是可行的,但是却额外开辟了容器本身的空间,而且容器删除后会存在缓存,且最要命的是——不同的Python解释器实现GC的机制都不一样,谁也说不清这么做会不会有什么奇怪的问题

为了解决不同Python解释器下对于变量的引用删除和堆空间删除问题,Python引入了弱引用和终结器的机制。这些机制都存放在内置库weakref中。


弱引用

weakref.ref

Python普通变量的引用会给堆变量增加引用计数,而弱引用有着和普通引用(后面称强引用)完全相同的使用方法,但是却不会增加堆变量的引用计数。

举个例子,如果我们使用强引用:

>>>importarray>>>importsys>>>a=array.array('i',[1,2,3])>>>sys.getrefcount(a)2>>>b=a>>>sys.getrefcount(a)3

可以看到,引用次数增加了。我们再来看看弱引用:

>>>importweakref>>>a=array.array('i',[1,2,3])>>>sys.getrefcount(a)2>>>b=weakref.ref(a)# weakref.ref(a)用于返回a指向的堆空间变量的弱引用>>>sys.getrefcount(a)2

可以看到使用弱引用并不会使引用计数器增加,我们用实线表示强引用,虚线表示弱引用,那么这个例子的变量模型如下:

事实上,b并不可以直接引用。这个类只实现了__call__方法。这意味着,我们调用b来获取指向的对象:

>>>b# 直接用是不行的<weakrefat0x00000252546D4548;to'array.array'at0x00000252546AAB70>>>>b()array('i',[1,2,3])

这样一来,只要确保除了a之外引用array的都是弱引用,我们就可以轻松完成之前那个需求了,只要删除a,那么a指向的堆变量就会直接释放,而如果弱引用指向的堆变量已经删除,那么调用弱引用对象就会返回None:

importweakrefimportarraya=array.array('i',[1,2,3])b=weakref.ref(a)delaprint(b())

out:

None
交互式环境使用上述语句会存在延迟,可能仍能返回有效的弱引用

需要注意!存在free_list缓存机制的基本数据类型不能被直接弱引用!如果你想要弱引用它们(比如列表),可以这么做:

>>>classMyList(list):.........>>>a=MyList([1,2,3])>>>a[1,2,3]>>>b=weakref.ref(a)

weakref.proxy

如果你嫌weakref.ref拿回来的对象需要调用才能拿到值,那么你可以直接使用weakref.proxy(a)获取a指向的堆变量的代理对象,这本质上也是一种弱引用,不过拿到的代理对象拥有和原对象完全相同的副本,能够直接使用:

>>>importarray>>>importweakref>>>a=array.array('i',[1,2,3])>>>b=weakref.proxy(a)>>>b<weakproxyat0x000001BEB26CCAE8toarray.arrayat0x000001BEB26D44F0>>>>print(b)array('i',[1,2,3])>>>set(dir(b))^set(dir(a))# 这说明a和b的属性方法完全一致set()

使用proxy后的变量模型如下所示:


weakref.WeakMethod

除了弱引用一个对象,weakref还提供了方法WeakMethod来允许你只弱引用用一个类中的某个方法:

importweakrefimportarrayclassMyObj:deffunc1(self):print("I am func1")deffunc2(self):print("I am func2")a=MyObj()method=weakref.WeakMethod(a.func1)method()()

out:

I am func1

这么做可以很好地限制引用变量对对象本身的访问权限。而且由于调用是不可被赋值的,所以外界无法轻易通过猴子补丁等方法覆盖原类的方法。


获取弱引用计数

通过以下两个方法即可获得关于弱引用的计数:

weakref.getweakrefcount(object):返回指向 object 的弱引用和代理的数量。

weakref.getweakrefs(object):返回由指向 object 的所有弱引用和代理构成的列表。

不过锦恢实际做下来发现,对于同一个对象的多次弱引用只被记作一次,对于同一个对象的多次代理只被记作一次。也就是说,weakref.getweakrefcount的返回值最多为2:

importweakrefimportarrayfrompprintimportpprinta=array.array('i',[1,2,3])b=weakref.proxy(a)c=weakref.proxy(a)d=weakref.ref(a)e=weakref.ref(a)print(weakref.getweakrefcount(a))pprint(weakref.getweakrefs(a))

out:

2[<weakrefat0x00000179A44D5F98;to'array.array'at0x00000179A454A2B0>,<weakproxyat0x00000179A4397688toarray.arrayat0x00000179A454A2B0>]

弱引用容器

我们都知道,在Python中,使用容器对象去装载对象也会增加对象的引用次数:

>>>importsys>>>a=[]>>>b=object()>>>sys.getrefcount(b)2>>>a.append(b)>>>sys.getrefcount(b)3

这一特性在对对象的生命周期进行管理时就会显得非常棘手,因为我们只希望管理这些对象,但是容器对于对象的收纳导致了额外的计数,那么这些对象的初始引用被del后,对象并不会被销毁。这就让我们的管理者容器与业务代码在内存管理上产生了强耦合。

这么说可能比较抽象,我们可以看个例子。假设我们现在写一个简单的医院的病员登记系统,创建病人代表病人住院,删除病人对象代表病人出院。病人入院时(对象初始化时)需要将对象放入一个全局哈希表中,一旦病人出院,发送通知“已出院”,并且主函数也会发送“已出院”,我们需要将病人从哈希表中删除,代码如下:

hospital_lists=set()classpatient:def__init__(self,name)->None:self.name=namehospital_lists.add(self)def__del__(self):print(self.name,"已出院")p1=patient("p1")p2=patient("p2")delp1print("p1 出院")delp2print("p2 出院")print("剩余病人数量:",len(hospital_lists))

out:

p1 出院
p2 出院
剩余病人数量: 2
p1 已出院
p2 已出院

由于哈希表的装载也会增加病人对象的引用,所以del不会将实际的病人对象从堆空间清除。所以产生了out中hospital_lists 不为空、病人和医院打印消息不同步的问题。

想要完成上面的任务,我们可以使用弱引用容器来完成,最简单的就是弱引用哈希表WeakSet,用法很简单,不同的地方在于:WeakSet对于外界对象的装载不会增加外界对象的引用计数,一旦外界对象引用计数器归零后,WeakSet会自动将其弱引用从哈希表中删除。

对于上述例子,我们只需要改一句话:

hospital_lists=weakref.WeakSet()

其他的不变,得到结果如下:

p1 已出院
p1 出院
p2 已出院
p2 出院
剩余病人数量: 0

结果很正确!

weakref还提供了WeakKeyDictionary和WeakValueDictionary,能够让dict在不污染对象计数的情况下去装载它们。锦恢在此处就不赘述了。


终结器

这是weakref最后一个没有讲的东西了,对于变量的引用和销毁,除了使用弱引用外,终结器时用来进行销毁回调的一大利器。

为了方便理解,对于对象A绑定的终结器作用几乎就等同于对象A的__del__方法。简单看个例子:

>>>importarray>>>a=array.array('i',[1,2,3])>>>fin=weakref.finalize(a,lambda:print("a is dead!"))>>>delaaisdead!

我们通过weakref.finalize创建了一个终结器,终结器一共有三个参数,第一个参数为需要绑定的对象,第二个参数为触发终结器时的回调函数,第三个参数是回调函数的参数。

终结器会在绑定的对象指向的堆变量被销毁时被调用。当然,在这之前,你可以随时调用它,但是终结器在它的生命周期中必定且只能被调用一次,这是对象实际空间被销毁的信号。

>>>a=array.array('i',[1,2,3])>>>fin=weakref.finalize(a,lambda:print("a is dead!"))>>>fin()aisdead!>>>fin()# 终结器只能被调用一次,所以不再触发回调>>>dela# 终结器只能被调用一次,所以不再触发回调

通过终结器对象的alive属性,可以随时查看绑定对象的堆变量是否存活,终结器被调用后,终结器的alive会被置为False:

>>>a=array.array('i',[1,2,3])>>>fin=weakref.finalize(a,lambda:print("a is dead!"))>>>fin2=weakref.finalize(a,lambda:print("a is dead2!"))>>>fin()aisdead!>>>fin.aliveFalse>>>fin2.aliveTrue>>>delaaisdead2!>>>fin2.aliveFalse

如果你突然后悔创建终结器了,可以使用detach方法来杀死一个终结器,它的alive会变为False,并返回元组(obj,func,args,kwargs):

>>>a=array.array('i',[1,2,3])>>>fin=weakref.finalize(a,lambda:print("a is dead!"))>>>fin.detach()(array('i',[1,2,3]),<function<lambda>at0x000001BEB269BE58>,(),{})>>>dela>>>fin.aliveFalse

所以读者看到这里肯定很疑惑,这些功能我都可以通过__del__实现呀,为什么要用终结器呢?锦恢有点累了,所以直接通过对比终结器的优势来解释吧。

可以为一个对象绑定多个终结器,从而在生命周期的不同时刻定义不同行为。

通过终结器可以间接反映对象的存活情况。

基于终结器的回调比__del__更加健壮

为什么说终结器的回调比__del__更加健壮呢?因为__del__受到运行环境影响,在有的Python解释器实现中,对象被销毁时并不会调用__del__,且__del__也会受到循环引用的影响。

除此之外,官方还给出了第四个优势:

简单说,你可以将创建的终结器交给第三方进行管理,实现更加灵活的内存管理。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值