Python弱引用原理与源码深度解析

Elena
Elena
发布时间:2026-10-15| 2人浏览过
原创

本文深入解析Python中weakref模块的工作原理。通过多线程缓存场景,阐述弱引用如何避免资源泄露,并介绍WeakValueDictionary等实用工具。进一步从C语言源码层面,剖析PyWeakReference结构体、观察者模式实现及链表管理机制,揭示弱引用在不增加引用计数前提下关联对象的底层逻辑。

背景

在探讨weakref模块之前,有必要先厘清弱引用的定义及其核心价值。

考虑一个多线程并发处理应用数据的场景:

# 占用大量资源,创建销毁成本很高\
class Data:\
    def __init__(self, key):\
        pass

应用数据Data由唯一的key标识,且可能被多个线程同时访问。由于Data对象占用大量系统资源,其创建和销毁成本极高。我们的目标是确保在整个程序中,同一份数据仅维护一个副本,即使被多个线程并发访问,也绝不重复创建。

基于此需求,我们尝试设计一个缓存中间件Cacher:

import threading
# 数据缓存
class Cacher:
    def __init__(self):
        self.pool = {}
        self.lock = threading.Lock()
    def get(self, key):
        with self.lock:
            data = self.pool.get(key)
            if data:
                return data
            self.pool[key] = data = Data(key)
            return data

Cacher内部使用一个dict对象来缓存已创建的Data副本,并提供get方法供外部获取数据。get方法的逻辑是:先查询缓存字典,若数据存在则直接返回;若不存在,则创建新实例并存入字典。这样,数据首次创建后便驻留于缓存字典中,后续其他线程访问时,均复用缓存中的同一个实例。

这种设计看似完美,但存在资源泄露的隐患!

问题在于,Data一旦创建并存入缓存字典,将永远持有强引用,导致其无法被垃圾回收。换言之,程序内存会持续增长,最终可能导致内存溢出。因此,我们需要一种机制:当所有线程不再访问某个数据时,该数据能自动释放。

一种解决方案是在Cacher中维护数据的引用计数,get方法自动累加计数,并提供一个新的remove方法用于释放数据。remove方法先递减引用计数,当计数降为零时,从缓存字典中删除该数据。

然而,这要求线程在获取数据后必须手动调用remove方法释放资源。Cacher相当于手动实现了引用计数法,这过于繁琐。既然Python内置了完善的垃圾回收机制,为何应用程序还需要自行实现引用计数管理呢?

冲突的核心症结在于Cacher的缓存字典:作为中间件,它本身并不消费数据对象,理论上不应持有对数据对象的强引用。那么,是否存在一种机制,能在不产生引用的前提下定位目标对象?我们知道,普通的赋值操作必然产生引用。

典型用法

此时,弱引用(weakref)应运而生。弱引用是一种特殊的对象类型,它能够在不增加目标对象引用计数的情况下,关联并访问目标对象。

# 创建一个数据
>>> d = Data('fasionchan.com')
>>> d
<__main__.Data object at 0x1018571f0>

# 创建一个指向该数据的弱引用
>>> import weakref
>>> r = weakref.ref(d)

# 调用弱引用对象,即可找到指向的对象
>>> r()
<__main__.Data object at 0x1018571f0>
>>> r() is d
True

# 删除临时变量d,Data对象就没有其他引用了,它将被回收
>>> del d
# 再次调用弱引用对象,发现目标Data对象已经不在了(返回None)
>>> r()

解决方案很简单:将Cacher的缓存字典改为存储弱引用,问题即可迎刃而解。

import threading
import weakref
# 数据缓存
class Cacher:
    def __init__(self):
        self.pool = {}
        self.lock = threading.Lock()
    def get(self, key):
        with self.lock:
            r = self.pool.get(key)
            if r:
                data = r()
                if data:
                    return data
            data = Data(key)
            self.pool[key] = weakref.ref(data)
            return data

由于缓存字典仅保存Data对象的弱引用,Cacher不会干扰Data对象的引用计数。当所有线程使用完数据后,Data对象的强引用计数降为零,触发垃圾回收机制将其释放。

实际上,使用字典缓存数据对象是常见做法。为此,weakref模块专门提供了两种仅保存弱引用的字典类型:

  • weakref.WeakKeyDictionary:一种映射类,其键仅保存弱引用(一旦某个键不再有其他强引用,对应的键值对条目将自动从字典中移除);
  • weakref.WeakValueDictionary:一种映射类,其值仅保存弱引用(一旦某个值不再有其他强引用,对应的键值对条目将自动从字典中移除);

因此,我们的数据缓存字典可以直接采用weakref.WeakValueDictionary来实现,其接口与普通字典完全一致。这样我们无需手动维护弱引用对象,代码逻辑更加简洁:

import threading
import weakref
# 数据缓存
class Cacher:
    def __init__(self):
        self.pool = weakref.WeakValueDictionary()
        self.lock = threading.Lock()
    def get(self, key):
        with self.lock:
            data = self.pool.get(key)
            if data:
                return data
            self.pool[key] = data = Data(key)
            return data

weakref模块还提供了许多其他有用的工具类和函数,具体细节建议参考官方文档,此处不再赘述。

工作原理

那么,弱引用究竟是如何实现这种“神奇”效果的?接下来,我们将揭开其面纱,深入底层原理。

>>> d = Data('fasionchan.com')

# weakref.ref 是一个内置类型对象
>>> from weakref import ref
>>> ref
<class 'weakref'>

# 调用weakref.ref类型对象,创建了一个弱引用实例对象
>>> r = ref(d)
>>> r
<weakref at 0x1008d5b80; to 'Data' at 0x100873d60>

基于前文对Python内置对象源码阅读经验的积累,相关的核心源码文件如下:

  • Include/weakrefobject.h 头文件定义了对象结构体及相关宏;
  • Objects/weakrefobject.c 源文件实现了弱引用类型对象及其方法;

首先分析弱引用对象的字段结构,定义位于Include/weakrefobject.h头文件的第10-41行:

typedef struct _PyWeakReference PyWeakReference;

/* PyWeakReference is the base struct for the Python ReferenceType, ProxyType,
 * and CallableProxyType.
 */
#ifndef Py_LIMITED_API
struct _PyWeakReference {
    PyObject_HEAD

    /* The object to which this is a weak reference, or Py_None if none.
     * Note that this is a stealth reference:  wr_object's refcount is
     * not incremented to reflect this pointer.
     */
    PyObject *wr_object;

    /* A callable to invoke when wr_object dies, or NULL if none. */
    PyObject *wr_callback;

    /* A cache for wr_object's hash code.  As usual for hashes, this is -1
     * if the hash code isn't known yet.
     */
    Py_hash_t hash;

    /* If wr_object is weakly referenced, wr_object has a doubly-linked NULL-
     * terminated list of weak references to it.  These are the list pointers.
     * If wr_object goes away, wr_object is set to Py_None, and these pointers
     * have no meaning then.
     */
    PyWeakReference *wr_prev;
    PyWeakReference *wr_next;
};
#endif

由此可见,PyWeakReference结构体构成了弱引用对象的实体。它是一个定长对象,除标准PyObject_HEAD头部外,还包含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头文件中提供了两个关键宏定义:

/* Test if a type supports weak references */
#define PyType_SUPPORTS_WEAKREFS(t) ((t)->tp_weaklistoffset > 0)

#define PyObject_GET_WEAKREFS_LISTPTR(o) \
    ((PyObject **) (((char *) (o)) + Py_TYPE(o)->tp_weaklistoffset))
  • PyType_SUPPORTS_WEAKREFS:用于判断类型对象是否支持弱引用。仅当tp_weaklistoffset大于零时,该类型才支持弱引用,内置类型如list等默认不支持;
  • PyObject_GET_WEAKREFS_LISTPTR:用于获取对象的弱引用链表头指针。它先通过Py_TYPE宏找到类型对象t,再结合tp_weaklistoffset确定偏移量,最后与对象地址相加得到链表头字段的地址;

创建弱引用时,我们需要调用weakref类型对象,并将被引用对象d作为参数传入。weakref类型对象是所有弱引用实例的元类型,是一个全局唯一的类型对象,定义在Objects/weakrefobject.c中,即_PyWeakref_RefType(第350行)。

根据Python对象模型,调用一个对象时执行的是其类型对象中的tp_call函数。因此,调用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行);
  • 调用PyObject_GET_WEAKREFS_LISTPTR宏获取目标对象的弱引用链表头指针,为方便插入操作,返回的是二级指针(第294行);
  • 调用get_basic_refs函数获取链表中第一个回调函数(callback)为空的“基础弱引用”对象(如有,第295行);
  • 如果新创建的弱引用没有回调函数,且链表中已存在一个回调为空的“基础弱引用”,则直接复用该实例并返回(第296行);
  • 若无法复用,则调用tp_alloc函数分配内存、初始化字段,并将其插入到目标对象的弱引用链表(第309行);
    • 如果新弱引用的回调函数为空,则将其插入到链表最前端,以便后续复用(见第4点逻辑);
    • 如果新弱引用的回调函数非空,则将其插入到“基础弱引用”对象(如有)之后,确保“基础弱引用”始终位于链表头部,便于快速获取;

当目标对象被回收时,tp_dealloc函数将调用PyObject_ClearWeakRefs函数对关联的弱引用进行清理。该函数获取目标对象的弱引用链表,逐个遍历,将wr_object字段置为None,并执行wr_callback回调函数(如果存在)。具体细节不再展开,有兴趣者可自行查阅Objects/weakrefobject.c源码第880行。

至此,我们彻底掌握了弱引用的相关知识。弱引用允许在不增加引用计数的情况下对目标对象进行管理,常用于框架和中间件开发。虽然看似神奇,但其设计原理实为简单的观察者模式。弱引用对象创建后,会插入到由目标对象维护的链表中,以订阅(观察)目标对象的销毁事件。

总结

本文介绍了Python中弱引用的用法与原理。更多关于Python弱引用的内容,欢迎搜索本站历史文章或浏览后续相关文章,感谢大家的支持!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。