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 |
A partire dal 1 gennaio 2021 le versioni 2.x di Python non sono piu' supportate. Ti invito a consultare la corrispondente versione 3.x dell'articolo per il modulo weakref
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.
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
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
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, inprint 'via proxy:', p.name ReferenceError: weakly-referenced object no longer exists
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:[]
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