Python で弱参照を使用する方法

PHPz
PHPz転載
2023-05-12 23:52:111434ブラウズ

背景

弱参照 (weakref) について説明する前に、まず弱参照とは何なのかを見てみましょう。正確には何をするのでしょうか?

アプリケーション データを同時に処理するマルチスレッド プログラムがあるとします。

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

アプリケーション データ データはキーによって一意に識別され、同じデータは複数のスレッドによって同時にアクセスされる可能性があります。時間。データは大量のシステム リソースを必要とするため、作成と使用のコストが高くなります。 Data はプログラム内でコピーを 1 つだけ保持し、複数のスレッドから同時にアクセスされた場合でもコピーを繰り返し作成しないことを希望します。

この目的を達成するために、キャッシュ ミドルウェア 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 コピーをキャッシュし、アプリケーション データ Data を取得するための get メソッドを提供します。 get メソッドはデータを取得する際、まずキャッシュ ディクショナリを確認し、データが存在する場合は直接返し、データが存在しない場合はキャッシュ ディクショナリを作成してディクショナリに保存します。したがって、データは最初に作成された後にキャッシュ ディクショナリに入力され、後で他のスレッドが同時にアクセスした場合、キャッシュ内の同じコピーが使用されます。

とても気持ちいいです!しかし問題は、Cacher にはリソース漏洩のリスクがあるということです。

データは作成されるとキャッシュ ディクショナリに保存され、解放されることはありません。言い換えれば、メモリなどのプログラムのリソースは増加し続け、最終的には爆発的に増加する可能性があります。したがって、すべてのスレッドがデータにアクセスしなくなった後に、データが自動的に解放されることを期待しています。

Cacher ではデータ参照の数を維持でき、get メソッドはこの数を自動的に蓄積します。同時に、データを解放するための新しい Remove メソッドが提供され、最初に参照数をデクリメントし、参照数が 0 になったときにキャッシュ フィールドからデータを削除します。

スレッドは get メソッドを呼び出してデータを取得します。データが使い果たされたら、remove メソッドを呼び出して解放する必要があります。 Cacher は参照カウント方法そのものを実装することに相当しますが、これは面倒すぎます。 Python にはガベージ コレクション メカニズムが組み込まれているのではないでしょうか?なぜアプリケーション自体がそれを実装する必要があるのでしょうか?

競合の主な核心は、Cacher のキャッシュ ディクショナリにあります。ミドルウェアとして、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

キャッシュ ディクショナリはデータ オブジェクトへの弱い参照のみを保存するため、Cacher はデータ オブジェクトの参照数に影響を与えません。すべてのスレッドがデータの使用を終了すると、参照カウントはゼロに低下し、解放されます。

実際には、データ オブジェクトをキャッシュするために辞書を使用するのが非常に一般的です。このため、weakref モジュールには、弱い参照のみを保存する 2 つの辞書オブジェクトも用意されています。 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構造体は弱参照オブジェクトの本体です。これは固定長のオブジェクトであり、固定ヘッダーに加えて、

##wr_object、オブジェクト ポインター、参照先オブジェクトを指す 5 つのフィールドがあります。 、弱い参照は、このフィールドに基づいて参照先オブジェクトを見つけることができますが、参照は生成されません。呼び出し可能なオブジェクトを指す

Python で弱参照を使用する方法

    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 行);


    • コールバックが空の場合は、その後の再利用を容易にするためにリンク リストの先頭に直接挿入します (ポイント 4 を参照)。コールバックが空でない場合は、基本的な弱参照オブジェクト (存在する場合) の後に挿入して、簡単にアクセスできるように基本的な弱参照がリンク リストの先頭にあるようにします。 ##オブジェクトがリサイクルされると、 tp_dealloc 関数は PyObject_ClearWeakRefs 関数を呼び出して、その弱い参照をクリーンアップします。この関数は、オブジェクトの弱参照リストを取り出し、それを 1 つずつ走査し、wr_object フィールドを消去し、wr_callback コールバック関数 (存在する場合) を実行します。詳細については展開されませんが、興味があれば、Objects/weakrefobject.c の 880 行目にあるソース コードを確認してください。

    • さて、このセクションを学習した後、私たちは弱参照に関連する知識を完全に習得しました。弱参照は、参照カウントを生成せずにターゲット オブジェクトを管理でき、フレームワークやミドルウェアでよく使用されます。弱参照は魔法のように見えますが、実際の設計原則は非常に単純なオブザーバー パターンです。弱参照オブジェクトが作成された後、それはターゲット オブジェクトによって維持されるリンク リストに挿入され、オブジェクトの破棄イベントが監視 (サブスクライブ) されます。

以上がPython で弱参照を使用する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。