背景在开始讨论弱引用(weakref)之前,我们先来看看什么是弱引用?它到底是做什么的?假设我们有一个并发处理应用数据的多线程程序:#占用大量资源,创建和销毁的成本非常高classData:def__init__(self,key):passkey,同一个数据可能被多个线程同时访问。由于Data需要大量的系统资源,创建和消费的成本都很高。我们希望程序中只维护一份Data,即使多个线程同时访问,我们也不希望重复创建。为此,我们尝试设计一个缓存中间件Cacher:data=self.pool.get(key)ifdata:returndataself.pool[key]=data=Data(key)returndataCacher内部使用了一个dict对象来缓存Data的创建副本,并提供了get方法获取应用数据Data。get方法获取数据时,首先检查缓存字典。如果数据已经存在,则直接返回;如果数据不存在,它会创建一个并将其保存在字典中。因此,数据在第一次创建后,进入缓存字典。如果其他线程同时访问它,则使用缓存中的相同副本。感觉很好!但美中不足的是:Cacher存在资源泄露的风险!因为Data一旦创建,就保存在缓存字典中,永远不会被释放!也就是说,程序的资源,比如内存,会不断增长,最后很可能会爆发。因此,我们希望一个数据能够在所有线程不再访问它之后自动释放。我们可以在Cacher中维护数据的引用计数,get方法会自动累加这个计数。同时提供了一个新的remove方法来释放数据。它首先递减引用计数,并在引用计数降为零时从缓存字段中删除数据。线程调用get方法获取数据,数据用完后需要调用remove方法释放。Cacher相当于自己实现了引用计数的方法,太麻烦了!Python不是有内置的垃圾回收机制吗?为什么应用需要自己实现?矛盾的主要症结在于Cacher的缓存字典:作为一个中间件,它本身并没有使用数据对象,所以理论上应该没有对数据的引用。有没有不用生成引用也能找到目标对象的黑科技?我们知道作业会产生参考!典型用法这时候,弱引用(weakref)就派上用场了!弱引用是一种特殊的对象,可以在不生成引用的情况下与目标对象相关联。#创建一个数据>>>d=Data('fasionchan.com')>>>d<__main__.Dataobjectat0x1018571f0>#创建一个指向数据的弱引用>>>importweakref>>>r=weakref.ref(d)#调用弱引用对象,可以找到指向的对象>>>r()<__main__.Dataobjectat0x1018571f0>>>>r()isdTrue#删除临时变量d,Data对象就没有其他引用了,会是recycled>>>deld#再次调用weak引用对象,发现目标Data对象已经不存在了(返回None)>>>r()这样我们只需要改变Cacher缓存字典来存放weak参考一下,问题就解决了!importthreadingimportweakref#数据缓存类Cacher:def__init__(self):self.pool={}self.lock=threading.Lock()defget(self,key):withself.lock:r=self.pool.get(key)ifr:data=r()ifdata:returndatadata=Data(key)self.pool[key]=weakref.ref(data)returndata由于缓存字典只保存Data对象的弱引用,Cacher不会影响Data的引用计数目的。当所有线程都处理完数据后,引用计数将降为零并被释放。事实上,使用字典来缓存数据对象是很常见的。为此,weakref模块还提供了两种只存储弱引用的字典对象:weakref.WeakKeyDictionary。,键值对条目将自动消失);weakref.WeakValueDictionary,值只保存弱引用的映射类(一旦值不再有强引用,键值对条目将自动消失);因此,我们的数据缓存字典可以使用weakref.WeakValueDictionary来实现,其接口与普通字典完全一样。这样我们就不用再自己维护弱引用对象了,代码逻辑也更加简洁明了:threading.Lock()defget(self,key):withself.lock:data=self.pool.get(key)ifdata:returndataself.pool[key]=data=Data(key)returndataweakref有很多有用的工具类和工具模块中的函数。具体可以参考官方文档,这里不再赘述。工作原理那么,弱引用究竟有何神圣之处,又为何会有如此神奇的力量呢?下面,就让我们揭开它的面纱,一睹它的真面目吧!>>>d=Data('fasionchan.com')#weakref.ref是内置类型对象>>>fromweakrefimportref>>>ref#调用weakref.ref类型对象,创建weakreferenceinstanceObject>>>r=ref(d)>>>r经过前面的章节,我们熟悉了阅读内置对象的源码。相关源码文件如下:Include/weakrefobject.h头文件包含对象结构和一些宏定义;objects/weakrefobject.c源文件包含弱引用类型对象及其方法定义;我们先看一下弱引用对象的字段结构,定义在Include/weakrefobject.h的头文件中10-41行:typedefstruct_PyWeakReferencePyWeakReference;/*PyWeakReferenceisthebasestructforthePythonReferenceType,ProxyType,*andCallableProxyType.*/#ifndefPy_LIMITED_APIstruct_PyWeakReference/PyObject_HEAD*这是弱引用的对象,或Py_Noneifnone。*请注意,这是一个静态引用:wr_object的引用计数*不递增以反映此指针。*/PyObject*wr_object;/*当wr_object死亡时可调用,或NULLifnone。*/PyObject*wr_callback;/*Acacheforwr_object的哈希码。通常用于哈希,thisis-1*ifthehashcodeisn'tknownyet.*/Py_hash_thash;/*Ifwr_objectisweaklyreferenced,wr_objecthasadoubly-linkedNULL-*terminatedlistofweakreferencestoit.Thesearethelistpointers.*Ifwr_objectgoesaway,wr_objectissettoPy_None,andthesepointers*havenomeaningthen.*/PyWeakReference*wr_prev;PyWeakReference*wr_next;};#endif由此可以看出PyWeakReference结构体就是弱引用对象的主体。它是一个固定长度的对象。除了固定头外,还有5个字段:wr_object,对象指针,指向被引用对象,弱引用可以根据这个字段找到被引用对象,但不会产生引用;wr_callback指向一个可调用对象,当被引用对象被销毁时会被调用;hash缓存引用对象的哈希值;wr_prev和wr_next分别将前向和后向引用对象组织成一个双向链表;结合代码中的注释,我们知道:弱引用对象是通过wr_object字段与被引用对象相关联的,如上图虚线箭头所示;一个对象可以同时关联多个弱引用对象,如图Data实例对象关联两个弱引用对象;所有与同一个对象关联的弱引用组织成一个双向链表,链表的头部存放在被引用的对象中,如上图实线箭头所示;当一个对象被销毁时,Python会遍历它的弱引用链表,并对其进行一一处理:将wr_object字段设置为None,弱引用对象再次被调用时将返回None,调用者就会知道该对象已被销毁;执行回调函数wr_callback(如果有的话);可见,弱引用的工作原理其实就是设计模式中的观察者模式(Observer)。当一个对象被销毁时,它的所有弱引用对象都会被通知并被妥善处理。实现细节掌握了弱引用的基本原理,就足以让我们用好它们了。如果您对源代码感兴趣,您还可以深入研究它的一些实现细节。前面我们提到,对同一个对象的所有弱引用组织成一个双向链表,链表的头部存放在对象中。由于可以创建弱引用的对象类型多种多样,很难用固定的结构来表示。因此,Python在类型对象中提供了一个字段tp_weaklistoffset,用于记录弱引用链表头指针在实例对象中的偏移量。这样一来,对于任意一个对象o,我们只需要通过ob_type字段找到它的类型对象t,然后根据t中的tp_weaklistoffset字段找到对象o的弱引用表头即可。Python在Include/objimpl.h头文件中提供了两个宏定义:/*Testifatypesupportsweakreferences*/#definePyType_SUPPORTS_WEAKREFS(t)((t)->tp_weaklistoffset>0)#definePyObject_GET_WEAKREFS_LISTPTR(o)\((PyObject**)(((char*)(o))+Py_TYPE(o)->tp_weaklistoffset))PyType_SUPPORTS_WEAKREFS用于判断类型对象是否支持弱引用。只有tp_weaklistoffset大于零时才能支持弱引用。内置对象列表不支持弱引用;PyObject_GET_WEAKREFS_LISTPTR用于获取对象的弱引用链表头。它首先通过Py_TYPE宏找到类型对象t,然后通过tp_weaklistoffset字段找到偏移量,最后加上对象的地址得到链表头字段的地址;我们创建弱引用时,需要调用弱引用类型对象weakref,并将被引用对象d作为参数传递。弱引用类型对象weakref是所有弱引用实例对象的类型,是Objects/weakrefobject.c中定义的全局唯一类型对象,即:_PyWeakref_RefType(第350行)。根据在对象模型中学到的知识,Python在调用一个对象时,会执行其类型对象中的tp_call函数。因此,当一个弱引用类型对象weakref被调用时,会执行该weakref的类型对象,即type的tp_call函数。tp_call函数回头调用weakref的tp_new和tp_init函数,其中tp_new为实例对象分配内存,tp_init负责初始化实例对象。回到Objects/weakrefobject.c源文件,可以看到_PyWeakref_RefType的tp_new字段被初始化为weakref___new__(第276行)。该函数的主要处理逻辑如下:解析参数得到引用对象(282行);调用PyType_SUPPORTS_WEAKREFS宏判断被引用对象是否支持弱引用,不支持则抛出异常(第286行);调用GET_WEAKREFS_LISTPTR获取对象的弱引用链表的头域,返回一个二级指针,方便插入(第294行);调用get_basic_refs取出链表顶部callback为空的基本弱引用对象(如果有,295行);如果回调为空,并且该对象有一个回调为空的基本弱引用,则重用该实例并直接返回(第296行);如果不能重用,则调用tp_alloc函数分配内存,完成字段初始化,插入到对象的弱引用链表中(309行);如果callback为空,则直接插入到链表的最前面,以供后续复用(见第4点);如果回调不为空,则在底层弱引用对象(如果有的话)之后插入,保证基础弱引用在链表头部,方便访问;当一个对象被回收时,tp_dealloc函数会调用PyObject_ClearWeakRefs函数来清理它的弱引用。该函数取出对象的弱引用链表,然后逐一遍历,清空wr_object字段并执行wr_callback回调函数(如果有的话)。具体细节就不展开了。有兴趣的可以参考Objects/weakrefobject.c中的源码,位于880行。好了,经过这一节的学习,我们已经彻底掌握了弱引用相关的知识。弱引用可以在不产生引用计数的情况下管理目标对象,常用于框架和中间件中。弱引用看起来很神奇,但设计原理是一个非常简单的观察者模式。弱引用对象创建后,被插入到目标对象维护的一个链表中,观察(订阅)对象的销毁事件。