Autour du codeDevelopper toujours mieux
Posté le

Python et les weakref

Les weakref sont comme des variables, elles sont une reference sur un objet python. La différence, c'est que les weakref n'affectent pas le compteur de référence de celui-ci.

Mais le compteur de référence, c'est quoi ?

En python, chaque fois qu'une variable pointe sur un objet, le conteur de référence est incrémentée de +1 et il est décrémenté lorsqu'une variable ne pointe plus vers lui. Lorsque le compteur de référence est à 0, la mémoire de l'objet sera libérée par le garbage collector, une sorte de râteau qui supprime de la mémoire tout ce qui n'a plus lieu d'être.

On peut observer cela avec la fonction getrefcount, qui retourne le nombre de référence qui pointe sur un objet. Par contre comme le fait d'appeler getrefcount sur l'objet crée une référence à celui-ci via une variable temporaire, getrefcount retourne une valeur de plus à celle attendue.

from sys import getrefcount

class Foo:
    def __init__(self, label):
        self.label = label
    def __del__(self):
        print("{} is destroy".format(self.label))

a = Foo('Toto') # la variable 'a' référe l'objet 'Toto'
getrefcount(a) # 2
b = a # 'b' référe aussi l'objet 'Toto'
getrefcount(b) # 3
a = 1
getrefcount(b) # 2
b = 2  # plus rien ne pointe sur l'objet Toto. Toto is destroy est affichée.

Et les weakref dans tout ça ?

Elles permettent d'avoir une référence vers un objet sans incrémenter son compteur. Demonstration:

>>> a = Foo('toto')
>>> getrefcount(a)
2
>>> ref_on_a  = weakref.ref(a) # Création d'une référence.
>>> getrefcount(a)
2
>>> ref_on_a() is a # Les ref sont des callables, on utilise () pour récupérer l'objet.
True
>>> del a
toto is destroy
>>> ref_on_a() is None # Si l'objet référencé est détruit, l'appel d'une ref retourne None.
True

Dans quel cas ça s'utilise ?

Le problème vient lorsque des objets se font mutuellement référencent.

class Foo:
    def __init__(self, label):
        self.label = label
        self._foo = None

    def set_foo(self, foo):
        self.foo = foo

    def __del__(self):
        print("{} is destroy".format(self.label))

a = Foo('toto')
b = Foo('tata')
a.set_foo(b)
b.set_foo(a)
del a
del b

Les compteurs de référence sur toto et tata ne sont pas à 0 est le garbage collector a plus de dificulté à libèrer la mémoire. C'est dans ce genre de cas que l'on peut utiliser les weakref.

import weakref

class Foo:
    def __init__(self, label):
        self.label = label
        self._foo = None

    def set_foo(self, foo):
        self.foo = weakref.ref(foo)

    def __del__(self):
        print("{} is destroy".format(self.label))

a = Foo('toto')
b = Foo('tata')
a.set_foo(b)
b.set_foo(a)
del a # toto is destroy
del b # tata is destroy

Attention au piège dans les environnements multithread.

if my_ref() is not None:
    my_ref().a_method()  #  Si entre temps l'objet pointé par my_ref est détruit,
                         #  la méthode a_method() est appelée sur None.

Il existe une autre façon de faire des références, les proxy.

>>> a = Foo('toto')
>>> p = weakref.proxy(a)
>>> p.label # Tout les attribut de toto sont accécible via le proxy.
'toto'
>>> del a
toto is destroy
>>> p
<weakproxy at 0x7f4704beeaa0 to NoneType at 0x890b40>
>>> p.label # Si l'objet référencé est détruit, l'accé a un attribut lance une exception ReferenceError.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ReferenceError: weakly-referenced object no longer exists

Une autre utilisation sont les systèmes de cache. Un objet couteux à créer est demandé. Cet objet est instancier est mise en cache. Si une autre personne demande l'objet le cache fournit l'objet si plus personne n'utilise l'objet, le cache est vidé automatiquement. Les classes WeakValueDictionary et WeakSet peuvent vous aider à implémenter ces caches