weakref - Riferimenti ad oggetti raccoglibili dal garbage collector

Scopo Fa riferimento ad un oggetto, ma consente ad esso di essere raccolto dal garbage collector se non ci sono altri riferimenti non deboli
Versione Python 2.1 e superiore
In informatica, un riferimento debole (weak reference) è un riferimento che non protegge l'oggetto referenziato dall'essere raccolto da un garbage collector al contrario di un riferimento forte (strong reference). Un oggetto referenziato solo da un riferimento debole, vale a dire che "ogni catena di riferimenti che raggiunge l'oggetto comprende almeno un riferimento debole come collegamento", viene considerato weakly reachable e può essere trattato come irraggiungibile, pertanto può essere raccolto dal garbage collector in qualsiasi momento. Python, assieme ad altri linguaggi (Java, C#, Perl, Lisp, Shell) ha un garbage collector che supporta vari livelli di riferimento debole.

Il modulo weakref supporta riferimenti deboli di oggetti. Un normale riferimento incrementa il contatore dei riferimenti sull oggetto e lo preserva dall'essere raccolto dal garbage collector. La qual cosa non è sempre desiderabile, sia quando possa essere presente un riferimento circolare oppure quando si costruisce una cache di oggetti che dovrebbero essere eliminati quando è necessaria della memoria.

Riferimenti

I riferimenti deboli verso i propri oggetti sono gestiti tramite la classe ref. Per recuperare l'oggetto originale, chiamare l'oggetto referenziato.

import weakref

class ExpensiveObject(object):
    def __del__(self):
        print '(In eliminazione %s)' % self

obj = ExpensiveObject()
r = weakref.ref(obj)

print 'obj:', obj
print 'ref:', r
print 'r():', r()

print 'eliminazione di obj'
del obj
print 'r():', r()

In questo caso, visto che obj viene eliminato prima della seconda chiamata al riferimento, ref ritorna None.

$ python weakref_ref.py

obj: <__main__.ExpensiveObject object at 0x7fa1bce4ead0>
ref: 
r(): <__main__.ExpensiveObject object at 0x7fa1bce4ead0>
eliminazione di obj
(In eliminazione <__main__.ExpensiveObject object at 0x7fa1bce4ead0>)
r(): None

Riferimenti a Callback

Il costruttore di ref ottiene un secondo argomento (opzionale) che dovrebbe essere una funzione di callback da chiamare quando l'oggetto referenziato viene eliminato.

import weakref

class ExpensiveObject(object):
    def __del__(self):
        print '(Eliminazione di %s)' % self

def callback(reference):
    """Chiamato quando l'oggetto referenziato viene eliminato"""
    print 'callback(', reference, ')'

obj = ExpensiveObject()
r = weakref.ref(obj, callback)

print 'obj:', obj
print 'ref:', r
print 'r():', r()

print 'Eliminazione di obj'
del obj
print 'r():', r()

La funzione di callback riceve il riferimento all'oggetto come argomento, dopo il riferimento è "morto" e non si riferisce più all'oggetto originale. Questo consente di rimuovere l'oggetto con riferimento debole da una cache, ad esempio.

$ python weakref_ref_callback.py

obj: <__main__.ExpensiveObject object at 0x7f4a24609d10>
ref: 
r(): <__main__.ExpensiveObject object at 0x7f4a24609d10>
Eliminazione di obj
callback(  )
(Eliminazione di <__main__.ExpensiveObject object at 0x7f4a24609d10>)
r(): None

Proxy

Invece di utilizzare ref direttamente, potrebbe essere più conveniente utilizzare un proxy. I proxy possono essere utilizzati come se fossero l'oggetto originale, quindi non serve prima chiamare ref per accedere all'oggetto.

import weakref

class ExpensiveObject(object):
    def __init__(self, name):
        self.name = name
    def __del__(self):
        print '(Eliminazione di %s)' % self

obj = ExpensiveObject('Il mio oggetto')
r = weakref.ref(obj)
p = weakref.proxy(obj)

print 'via obj:', obj.name
print 'via ref:', r().name
print 'via proxy:', p.name
del obj
print 'via proxy:', p.name

Se si accede al proxy dopo che l'oggetto a cui si riferisce è rimosso, viene sollevata una eccezione ReferenceError

$ python weakref_proxy.py

via obj: Il mio oggetto
via ref: Il mio oggetto
via proxy: Il mio oggetto
(Eliminazione di <__main__.ExpensiveObject object at 0x7f7c81ffee90>)
via proxy:
Traceback (most recent call last):
  File "weakref_proxy.py", line 20, in 
    print 'via proxy:', p.name
ReferenceError: weakly-referenced object no longer exists

Riferimenti Ciclici

Un utilizzo per i riferimenti deboli è di consentire riferimenti ciclici senza impedire la raccolta dal garbage collector. Questo esempio illustra la differenza tra l'utilizzo di oggetti normali e di proxy quando un grafo comprende un ciclo.

Per prima cosa occorre una classe Graph che accetti un qualsiasi oggetto come prossimo ("next") nodo nella sequenza. Per amor di brevità, Graph supporta un riferimento singolo in uscita da ogni nodo, il che produce grafi noiosi ma rende facile la creazione di cicli. La funzione demo() è una funzione di convenienza per far sì che la classe Graph crei un ciclo e quindi rimuova i vari riferimenti.

    import gc
from pprint import pprint
import weakref

class Graph(object):
    def __init__(self, name):
        self.name = name
        self.other = None
    def set_next(self, other):
        print '%s.set_next(%s (%s))' % (self.name, other, type(other))
        self.other = other
    def all_nodes(self):
        "Genera i nodi nella sequenza del grafo."
        yield self
        n = self.other
        while n and n.name != self.name:
            yield n
            n = n.other
        if n is self:
            yield n
        return
    def __str__(self):
        return '->'.join([n.name for n in self.all_nodes()])
    def __repr__(self):
        return '%s(%s)' % (self.__class__.__name__, self.name)
    def __del__(self):
        print '(Eliminazione di  %s)' % self.name
        self.set_next(None)

class WeakGraph(Graph):
    def set_next(self, other):
        if other is not None:
            # Verificare se si debba sostituire il riferimento ad
            # other con una weakref.
            if self in other.all_nodes():
                other = weakref.proxy(other)
        super(WeakGraph, self).set_next(other)
        return

def collect_and_show_garbage():
    "Mostra che garbage è presente."
    print 'Raccolta...'
    n = gc.collect()
    print 'Oggetti irraggiungili:', n
    print 'Garbage:',
    pprint(gc.garbage)

def demo(graph_factory):
    print 'Impostazione del grafo'
    one = graph_factory('uno')
    two = graph_factory('due')
    three = graph_factory('tre')
    one.set_next(two)
    two.set_next(three)
    three.set_next(one)

    print
    print 'Grafi :'
    print str(one)
    print str(two)
    print str(three)
    collect_and_show_garbage()

    print
    three = None
    two = None
    print 'Dopo la rimozione di 2 riferimenti:'
    print str(one)
    collect_and_show_garbage()

    print
    print "Rimozione dell'ultimo riferimento"
    one = None
    collect_and_show_garbage()

Si imposta un programma di test utilizzando il modulo gc per aiutare il debug della perdita. Il flag DEBUG_LEAK fa sì che gc stampi informazioni sugli oggetti che non possono essere visti in altro modo se non tramite il riferimento ad essi che ha il garbage collector

import gc
from pprint import pprint
import weakref

from weakref_graph import Graph, demo, collect_and_show_garbage

gc.set_debug(gc.DEBUG_LEAK)

print 'Impostazione del ciclo'
print
demo(Graph)

print
print 'Interruzione del ciclo e pulizia del garbage'
print
gc.garbage[0].set_next(None)
while gc.garbage:
    del gc.garbage[0]
print
collect_and_show_garbage()

Anche dopo l'eliminazione dei riferimenti locali alle istanze di Graph in demo() i grafi si trovano nella lista del garbage e non possono essere raccolti. I dizionari nel garbage mantengono gli attributi alle istanze di Graph. E' possibile forzare la cancellazione dei grafi, visto che si sa cosa sono.

$ python weakref_cycle.py

Impostazione del ciclo

Impostazione del grafo
uno.set_next(due ())
due.set_next(tre ())
tre.set_next(uno->due->tre ())

Grafi :
uno->due->tre->uno
due->tre->uno->due
tre->uno->due->tre
Raccolta...
Oggetti irraggiungili: 0
Garbage:[]

Dopo la rimozione di 2 riferimenti:
uno->due->tre->uno
Raccolta...
Oggetti irraggiungili: 0
Garbage:[]

Rimozione dell'ultimo riferimento
Raccolta...
gc: uncollectable 
gc: uncollectable 
gc: uncollectable 
gc: uncollectable 
gc: uncollectable 
gc: uncollectable 
Oggetti irraggiungili: 6
Garbage:[Graph(uno),
 Graph(due),
 Graph(tre),
 {'name': 'uno', 'other': Graph(due)},
 {'name': 'due', 'other': Graph(tre)},
 {'name': 'tre', 'other': Graph(uno)}]

Interruzione del ciclo e pulizia del garbage

uno.set_next(None ())
(Eliminazione di  due)
due.set_next(None ())
(Eliminazione di  tre)
tre.set_next(None ())
(Eliminazione di  uno)
uno.set_next(None ())

Raccolta...
Oggetti irraggiungili: 0
Garbage:[]

Ora si definisce una classe più intelligente WeakGraph che sappia come non creare cicli utilizzando riferimenti normali, ma utilizzando ref quando viene individuato un ciclo.

import gc
from pprint import pprint
import weakref

from weakref_graph import Graph, demo

class WeakGraph(Graph):
    def set_next(self, other):
        if other is not None:
            # Verificare se si debba sostituire il riferimento ad
            # other con una weakref.
            if self in other.all_nodes():
                other = weakref.proxy(other)
        super(WeakGraph, self).set_next(other)
        return

demo(WeakGraph)

Visto che le istanze di WeakGraph utilizzano proxy per riferirsi agli oggetti che hanno già visto, mentre demo() rimuove tutti i riferimenti locali agli oggetti, il ciclo viene spezzato ed il garbage collector può eliminare gli oggetti.

$ python weakref_weakgraph.py

Impostazione del grafo
uno.set_next(due ())
due.set_next(tre ())
tre.set_next(uno->due->tre ())

Grafi :
uno->due->tre
due->tre->uno->due
tre->uno->due->tre
Raccolta...
Oggetti irraggiungili: 0
Garbage:[]

Dopo la rimozione di 2 riferimenti:
uno->due->tre
Raccolta...
Oggetti irraggiungili: 0
Garbage:[]

Rimozione dell'ultimo riferimento
(Eliminazione di  uno)
uno.set_next(None ())
(Eliminazione di  due)
due.set_next(None ())
(Eliminazione di  tre)
tre.set_next(None ())
Raccolta...
Oggetti irraggiungili: 0
Garbage:[]

Cache degli Oggetti

Le classi ref e weakref sono considerate classi di "basso livello". Laddove esse sono utili per mantenere riferimenti deboli ad oggetti individuali e per consentire a cicli di essere raccolti dal garbage collector, se occorre creare una cache di parecchi oggetti WeakKeyDictionary e WeakValueDictionary forniscono API più appropriate.

Come ci si potrebbe aspettare, WeakValueDictionary utilizza riferimenti deboli come valori da conservare, consentendo di essere poi raccolti dal garbage collector quando non vengono più utilizzati da altre parti di codice.

Per dimostrare la differenza in termini di gestione di memoria rispetto ad un normale dizionario, si esperimenta chiamando ancora esplicitamente il garbage collector

import gc
from pprint import pprint
import weakref

gc.set_debug(gc.DEBUG_LEAK)

class ExpensiveObject(object):
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return 'ExpensiveObject(%s)' % self.name
    def __del__(self):
        print '(Deleting %s)' % self

def demo(cache_factory):
    # trattiene gli oggetti in modo che nessun riferimento debole
    # venga rimossa immediatamente
    all_refs = {}
    # La cache utilizza la factory che forniamo
    print 'TIPO CACHE:', cache_factory
    cache = cache_factory()
    for name in [ 'uno', 'due', 'tre' ]:
        o = ExpensiveObject(name)
        cache[name] = o
        all_refs[name] = o
        del o # decref

    print 'all_refs =',
    pprint(all_refs)
    print 'Prima, la cache contiene:', cache.keys()
    for name, value in cache.items():
        print '  %s = %s' % (name, value)
        del value # decref

    # Rimuove tutti i riferimenti ai nostri oggetti tranne la cache
    print 'Pulizia:'
    del all_refs
    gc.collect()

    print 'Dopo, la cache contiene:', cache.keys()
    for name, value in cache.items():
        print '  %s = %s' % (name, value)
    print 'demo in uscita'
    return

demo(dict)
print
demo(weakref.WeakValueDictionary)

Si noti che qualsiasi variabile di ciclo che fa riferimento ai valori oggetto di cache deve essere pulita esplicitamente per decrementare il conteggio di riferimenti sull'oggetto. Altrimenti il garbage collector non rimuoverebbe gli oggetti, che rimarrebbero nella cache. Alla stessa stregua, la variabile all_refs viene usata per mantenere riferimenti e prevenirne la raccolta prematura da parte del garbage collector.

$ python weakref_valuedict.py

TIPO CACHE: 
all_refs ={'due': ExpensiveObject(due),
 'tre': ExpensiveObject(tre),
 'uno': ExpensiveObject(uno)}
Prima, la cache contiene: ['tre', 'due', 'uno']
  tre = ExpensiveObject(tre)
  due = ExpensiveObject(due)
  uno = ExpensiveObject(uno)
Pulizia:
Dopo, la cache contiene: ['tre', 'due', 'uno']
  tre = ExpensiveObject(tre)
  due = ExpensiveObject(due)
  uno = ExpensiveObject(uno)
demo in uscita
(Deleting ExpensiveObject(tre))
(Deleting ExpensiveObject(due))
(Deleting ExpensiveObject(uno))

TIPO CACHE: weakref.WeakValueDictionary
all_refs ={'due': ExpensiveObject(due),
 'tre': ExpensiveObject(tre),
 'uno': ExpensiveObject(uno)}
Prima, la cache contiene: ['tre', 'due', 'uno']
  tre = ExpensiveObject(tre)
  due = ExpensiveObject(due)
  uno = ExpensiveObject(uno)
Pulizia:
(Deleting ExpensiveObject(tre))
(Deleting ExpensiveObject(due))
(Deleting ExpensiveObject(uno))
Dopo, la cache contiene: []
demo in uscita

WeakKeyDictionary lavora in modo simile ma utilizza riferimenti deboli per le chiavi invece che per i valori.

La documentazione della libreria per weakref contiene questo avvertimento

Cautela: Visto che WeakValueDictionary è costruito sopra un dizionario Python, non deve cambiare dimensione quando viene iterato. Questo potrebbe essere difficoltoso per un WeakValueDictionary visto che le azioni eseguite dal programma durante l'iterazione potrebbero far sì che elementi nel dizionario spariscano "magicamente" (come effetto collaterale del raccolta del garbage collector)

Vedere anche:

weakref
La documentazione della libreria standard per questo modulo.
gc
Il modulo gc è l'interfaccia per garbage collector dell'interprete