首頁  >  文章  >  後端開發  >  Python中弱引用怎麼使用

Python中弱引用怎麼使用

PHPz
PHPz轉載
2023-05-12 23:52:111386瀏覽

背景

開始討論弱引用( weakref )之前,我們先來看看什麼是弱引用?它到底有什麼作用?

假設我們有一個多執行緒程序,並發處理應用資料:

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

應用程式資料 Data 由一個 key 唯一標識,同一個資料可能被多個執行緒同時存取。由於 Data 需要佔用許多系統資源,創建和消費的成本很高。我們希望 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 方法來取得應用資料 Data 。 get 方法取得資料時先查快取字典,如果資料已存在,便直接傳回;如果資料不存在,則建立一個並儲存到字典中。因此,資料首次被創建後就進入快取字典,後續如有其它線程同時訪問,使用的都是快取中的同一個副本。

感覺非常好!但美中不足的是:Cacher 有資源外洩的風險!

因為 Data 一旦建立後,就保存在快取字典中,永遠不會釋放!換句話說,程式的資源例如內存,會不斷地成長,最終很有可能會爆掉。因此,我們希望一個資料等所有執行緒都不再存取後,能夠自動釋放。

我們可以在 Cacher 中維護資料的引用次數, get 方法自動累積這個數。同時提供一個 remove 新方法用於釋放數據,它先自減引用次數,並在引用次數降為零時將數據從快取字段中刪除。

執行緒呼叫 get 方法取得數據,資料用完後需要呼叫 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()

Python中弱引用怎麼使用

這樣一來,我們只要將 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 物件的參考計數。當所有執行緒都用完資料後,引用計數就降為零因而被釋放。

實際上,用字典快取資料物件的做法很常用,為此 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(&#39;fasionchan.com&#39;)

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

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

經過前面章節,我們對閱讀內建物件原始碼已經輕車熟路了,相關原始碼檔案如下:

  • 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&#39;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&#39;s hash code.  As usual for hashes, this is -1
     * if the hash code isn&#39;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 結構體便是弱引用對象的肉身。它是定長對象,除固定頭部外還有 5 個欄位:

Python中弱引用怎麼使用

  • #wr_object ,物件指針,指向被引用對象,弱引用根據該欄位可以找到被引用對象,但不會產生引用;

  • wr_callback ,指向一個可呼叫對象,當被引用的物件銷毀時將被呼叫;

  • hash ,缓存被引用对象的哈希值;

  • wr_prev 和 wr_next 分别是前后向指针,用于将弱引用对象组织成双向链表;

结合代码中的注释,我们知道:

Python中弱引用怎麼使用

  • 弱引用对象通过 wr_object 字段关联被引用的对象,如上图虚线箭头所示;

  • 一个对象可以同时被多个弱引用对象关联,图中的 Data 实例对象被两个弱引用对象关联;

  • 所有关联同一个对象的弱引用,被组织成一个双向链表,链表头保存在被引用对象中,如上图实线箭头所示;

  • 当一个对象被销毁后,Python 将遍历它的弱引用链表,逐一处理:


    • 将 wr_object 字段设为 None ,弱引用对象再被调用将返回 None ,调用者便知道对象已经被销毁了;

    • 执行回调函数 wr_callback (如有);

由此可见,弱引用的工作原理其实就是设计模式中的 观察者模式( Observer )。当对象被销毁,它的所有弱引用对象都得到通知,并被妥善处理。

实现细节

掌握弱引用的基本原理,足以让我们将其用好。如果您对源码感兴趣,还可以再深入研究它的一些实现细节。

前面我们提到,对同一对象的所有弱引用,被组织成一个双向链表,链表头保存在对象中。由于能够创建弱引用的对象类型是多种多样的,很难由一个固定的结构体来表示。因此,Python 在类型对象中提供一个字段 tp_weaklistoffset ,记录弱引用链表头指针在实例对象中的偏移量。

Python中弱引用怎麼使用

由此一来,对于任意对象 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中弱引用怎麼使用

根据对象模型中学到的知识,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 行);

  • 如果 callback 为空,而且对象存在 callback 为空的基础弱引用,则复用该实例直接将其返回(第 296 行);

  • 如果不能复用,调用 tp_alloc 函数分配内存、完成字段初始化,并插到对象的弱引用链表(第 309 行);


    • 如果callback 為空,直接將其插入到鍊錶最前面,方便後續復用(請參閱第4 點);

    • 如果callback 非空,將其插到基礎弱引用物件(如有)之後,保證基礎弱引用位於鍊錶頭,方便取得;

當一個物件被回收後,tp_dealloc 函數將呼叫 PyObject_ClearWeakRefs 函數對它的弱引用進行清理。此函數取出物件的弱引用鍊錶,然後逐一遍歷,清理 wr_object 欄位並執行 wr_callback 回調函數(如有)。具體細節不再展開,有興趣的話可以自行查閱 Objects/weakrefobject.c 中的源碼,位於 880 行。

好了,經過本節學習,我們徹底掌握了弱引用相關知識。弱引用可以在不產生引用計數的前提下,對目標物件進行管理,常用於框架和中介軟體中。弱引用看起來很神奇,其實設計原理是非常簡單的觀察者模式。弱引用物件建立後便插到一個由目標物件維護的鍊錶中,觀察(訂閱)物件的銷毀事件。

以上是Python中弱引用怎麼使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除