Maison > Article > développement back-end > Comment utiliser des références faibles en Python
Avant de commencer à discuter des références faibles (weakref), jetons d'abord un coup d'œil à ce qu'est une référence faible ? Ça fait quoi exactement ?
Supposons que nous ayons un programme multithread qui traite les données d'application simultanément :
# 占用大量资源,创建销毁成本很高\ class Data:\ def __init__(self, key):\ pass
Données d'application Les données sont identifiées de manière unique par une clé et les mêmes données peuvent être consultées par plusieurs threads en même temps. Les données nécessitant beaucoup de ressources système, le coût de création et de consommation est élevé. Nous espérons que Data ne conserve qu'une seule copie dans le programme et ne souhaite pas la créer à plusieurs reprises même si plusieurs threads y accèdent en même temps.
À cette fin, nous essayons de concevoir un middleware de mise en cache 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 utilise en interne un objet dict pour mettre en cache la copie de données créée et fournit une méthode get pour obtenir les données de l'application. Lorsque la méthode get obtient des données, elle vérifie d'abord le dictionnaire de cache. Si les données existent déjà, elles seront renvoyées directement ; si les données n'existent pas, elle en créera un et l'enregistrera dans le dictionnaire. Par conséquent, les données sont saisies dans le dictionnaire du cache après leur première création. Si d'autres threads y accèdent en même temps ultérieurement, la même copie dans le cache sera utilisée.
Ça fait du bien ! Mais le problème est que Cacher court un risque de fuite de ressources !
Car une fois les Données créées, elles sont stockées dans le dictionnaire cache et ne seront jamais publiées ! En d’autres termes, les ressources du programme, comme la mémoire, continueront de croître et pourraient éventuellement exploser. Par conséquent, nous espérons qu’une donnée pourra être automatiquement libérée une fois que tous les threads n’y auront plus accès.
Nous pouvons conserver le nombre de références de données dans Cacher, et la méthode get accumule automatiquement ce nombre. Dans le même temps, une nouvelle méthode de suppression est fournie pour libérer les données. Elle décrémente d'abord le nombre de références et supprime les données du champ de cache lorsque le nombre de références tombe à zéro.
Le fil appelle la méthode get pour obtenir les données. Une fois les données épuisées, la méthode delete doit être appelée pour les libérer. Cacher équivaut à implémenter la méthode de comptage de références elle-même, ce qui est trop gênant ! Python n'a-t-il pas un mécanisme de récupération de place intégré ? Pourquoi l’application doit-elle l’implémenter elle-même ?
Le nœud principal du conflit réside dans le dictionnaire de cache de Cacher : en tant que middleware, il n'utilise pas lui-même d'objets de données, donc théoriquement, il ne devrait pas avoir de références aux données. Existe-t-il donc une technologie noire capable de trouver l'objet cible sans générer de référence ? On le sait, les missions génèrent des références !
En ce moment, référence faible (faibleref) fait une grande apparition ! Une référence faible est un objet spécial qui peut être associé à l'objet cible sans générer de référence.
# 创建一个数据 >>> 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()
De cette façon, il suffit de changer le dictionnaire du cache Cacher pour sauvegarder les références faibles, et le problème sera résolu !
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
Étant donné que le dictionnaire de cache n'enregistre que les références faibles à l'objet Data, le Cacher n'affectera pas le nombre de références de l'objet Data. Lorsque tous les threads ont fini d'utiliser les données, le nombre de références tombe à zéro et est libéré.
En fait, il est très courant d'utiliser des dictionnaires pour mettre en cache des objets de données. Pour cette raison, le module lowref fournit également deux objets dictionnaire qui enregistrent uniquement les références faibles :
weakref.WeakKeyDictionary, la clé enregistre uniquement la classe de mappage. de références faibles (une fois que la clé n'a plus de référence forte, l'entrée de la paire clé-valeur disparaîtra automatiquement
weakref.WeakValueDictionary, la valeur enregistre uniquement la classe de mappage de référence faible (une fois que la valeur n'a plus) ; une référence forte, l'entrée de la paire clé-valeur disparaîtra automatiquement);
Par conséquent, notre dictionnaire de cache de données peut être implémenté à l'aide de faibleref.WeakValueDictionary, et son interface est exactement la même qu'un dictionnaire ordinaire. De cette façon, nous n'avons plus besoin de maintenir nous-mêmes des objets de référence faibles, et la logique du code est plus concise et claire : le module
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 possède également de nombreuses classes d'outils et fonctions d'outils utiles. Veuillez vous référer à la documentation officielle pour plus d'informations. détails et ne sera pas répété ici.
Alors, qui est exactement la référence faible, et pourquoi a-t-elle un tel pouvoir magique ? Ensuite, enlevons son voile et voyons sa véritable apparence !
>>> 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>
Après les chapitres précédents, nous sommes déjà familiarisés avec la lecture du code source des objets intégrés. Les fichiers de code source pertinents sont les suivants :
Include/weakrefobject.h Le fichier d'en-tête contient la structure de l'objet et certains. définitions de macro ;
Objects/weakrefobject Le fichier source .c contient des objets de type référence faible et leurs définitions de méthode
Jetons d'abord un coup d'œil à la structure de champ de l'objet de référence faible, qui est définie aux lignes 10 ; -41 dans le fichier d'en-tête Include/weakrefobject.h :
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
On peut voir que la structure PyWeakReference est le corps physique de l'objet de référence faible. C'est un objet de longueur fixe avec 5 champs en plus de l'en-tête fixe :
wr_object , pointeur d'objet, pointant vers l'objet référencé, une référence faible peut trouver l'objet référencé en fonction de ce champ, mais ne le fera pas être généré Référence ;
wr_callback, pointant vers un objet appelable, qui sera appelé lorsque l'objet référencé est détruit
;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 时,执行的是 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 行);
Si le rappel est vide, insérez-le directement au début de la liste chaînée pour faciliter la réutilisation ultérieure (voir point 4)
Si le rappel n'est pas vide, insérez-le dans l'objet de référence faible de base (); le cas échéant) Après cela, assurez-vous que la référence faible de base est située en tête de la liste chaînée pour un accès facile
Lorsqu'un objet est recyclé, la fonction tp_dealloc appellera la fonction PyObject_ClearWeakRefs pour nettoyer ses références faibles ; . Cette fonction supprime la liste de références faibles de l'objet, puis la parcourt une par une, nettoie le champ wr_object et exécute la fonction de rappel wr_callback (le cas échéant). Les détails spécifiques ne seront pas développés. Si vous êtes intéressé, vous pouvez vérifier le code source dans Objects/weakrefobject.c, situé à la ligne 880.
D'accord, après avoir étudié cette section, nous maîtrisons parfaitement les connaissances liées aux références faibles. Les références faibles peuvent gérer l'objet cible sans générer de décompte de références et sont souvent utilisées dans les frameworks et les middlewares. Les références faibles semblent magiques, mais en réalité, le principe de conception est un modèle d’observation très simple. Une fois l'objet de référence faible créé, il est inséré dans une liste chaînée maintenue par l'objet cible, et l'événement de destruction de l'objet est observé (souscrit).
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!