weakref - Riferimenti Non Permanenti a Oggetti
Scopo: Fa riferimento a un oggetto "costoso", ma consente che la sua memoria sia reclamata dal garbage collector se non ci sono altri riferimenti non deboli.
Il modulo weakref supporta riferimenti deboli a oggetti. Un normale riferimento incrementa il contatore dei riferimenti all'oggetto e lo preserva dall'essere raccolto dal garbage collector. Questo comportamento 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. Un riferimento debole è un puntamento a un oggetto che non lo esclude dell'essere pulito automaticamente.
Riferimenti
I riferimenti deboli verso gli oggetti sono gestiti tramite la classe ref
. Per recuperare l'oggetto originale, si chiami l'oggetto referenziato.
# weakref_ref.py
import weakref
class ExpensiveObject:
def __del__(self):
print('(In eliminazione {})'.format(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
.
$ python3 weakref_ref.py obj: <__main__.ExpensiveObject object at 0x7fe2b4c06a58> ref: <weakref at 0x7fe2b4c24318; to 'ExpensiveObject' at 0x7fe2b4c06a58> r(): <__main__.ExpensiveObject object at 0x7fe2b4c06a58> eliminazione di obj (In eliminazione <__main__.ExpensiveObject object at 0x7fe2b4c06a58>) r(): None
Callback su Riferimenti
Il costruttore ref
accetta una funzione callback opzionale da invocare quando l'oggetto referenziato viene cancellato.
# weakref_ref_callback.py
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 che il riferimento è "morto" e non si riferisce più all'oggetto originale. Un uso per questa caratteristica è per rimuovere l'oggetto con riferimento debole da una cache.
$ python3 weakref_ref_callback.py obj: <__main__.ExpensiveObject object at 0x7ff1e7296ac8> ref: <weakref at 0x7ff1e72b3368; to 'ExpensiveObject' at 0x7ff1e7296ac8> r(): <__main__.ExpensiveObject object at 0x7ff1e7296ac8> Eliminazione di obj (Eliminazione di <__main__.ExpensiveObject object at 0x7ff1e7296ac8>) callback( <weakref at 0x7ff1e72b3368; dead> ) r(): None
Finalizzare gli Oggetti
Per una gestione più robusta delle risorse quando vengono cancellati i riferimenti deboli si utilizzi finalize
per associare callback a oggetti. Una istanza di finalize
viene mantenuta fino a che l'oggetto attaccato a essa non viene cancellato, anche se l'applicazione non mantiene un riferimento all'istanza di finalize
.
# weakref_finalize.py
import weakref
class ExpensiveObject:
def __del__(self):
print('(In eliminazione {})'.format(self))
def on_finalize(*args):
print('on_finalize({!r})'.format(args))
obj = ExpensiveObject()
weakref.finalize(obj, on_finalize, 'argumento extra')
del obj
Gli argomenti per finalize
sono l'oggetto da tracciare, un callback da chiamare quando l'oggetto viene raccolto dal garbage collector, e qualsiasi argomento posizionale o nominale da passare al callback.
$ python3 weakref_finalize.py (In eliminazione <__main__.ExpensiveObject object at 0x7f1226f69a20>) on_finalize(('argumento extra',))
L'istanza di finalize
ha un proprietà scrivibile chiamata atexit
per controllare il callback vengo invocato mentre il programma è in uscita, se non è già stato chiamato.
# weakref_finalize_atexit.py
import sys
import weakref
class ExpensiveObject:
def __del__(self):
print('(In eliminazione {})'.format(self))
def on_finalize(*args):
print('on_finalize({!r})'.format(args))
obj = ExpensiveObject()
f = weakref.finalize(obj, on_finalize, 'argumento extra')
f.atexit = bool(int(sys.argv[1]))
Il comportamento predefinito è di chiamare il callback; impostando atexit
a falso disabilita questo comportamento.
$ python3 weakref_finalize_atexit.py 1 on_finalize(('argumento extra',)) (In eliminazione <__main__.ExpensiveObject object at 0x7fb41a23ba20>) $ python3 weakref_finalize_atexit.py 0
Passando all'istanza di finalize
un riferimento all'oggetto che traccia fa sì che il riferimento venga mantenuto, quindi l'oggetto non viene mai raccolto dal garbage collector.
# weakref_finalize_reference.py
import gc
import weakref
class ExpensiveObject:
def __del__(self):
print('(In eliminazione {})'.format(self))
def on_finalize(*args):
print('on_finalize({!r})'.format(args))
obj = ExpensiveObject()
obj_id = id(obj)
f = weakref.finalize(obj, on_finalize, obj)
f.atexit = False
del obj
for o in gc.get_objects():
if id(o) == obj_id:
print('trovato oggetto non raccoglibile in gc')
Questo esempio mostra che l'oggetto viene trattenuto e visibile dal garbage collector attraverso f
anche se è stato cancellato il riferimento esplicito a obj
.
$ python3 weakref_finalize_reference.py trovato oggetto non raccoglibile in gc
Utilizzando come callback un metodo legato alla classe dell'oggetto tracciato può inibire un oggetto dall'essere finalizzato propriamente.
# weakref_finalize_reference_method.py
import gc
import weakref
class ExpensiveObject:
def __del__(self):
print('(In eliminazione {})'.format(self))
def do_finalize(self):
print('do_finalize')
obj = ExpensiveObject()
obj_id = id(obj)
f = weakref.finalize(obj, obj.do_finalize)
f.atexit = False
del obj
for o in gc.get_objects():
if id(o) == obj_id:
print('trovato oggetto non raccoglibile in gc')
Visto che il callback passato a finalize
è un oggetto legato alla istanza di obj
, l'oggetto finalize mantiene un riferimento a obj, il quale non può essere eliminato e raccolto dal garbage collector.
$ python3 weakref_finalize_reference_method.py trovato oggetto non raccoglibile in gc
Proxy
Talvolta è più conveniente utilizzare un proxy al posto di un riferimento debole. I proxy possono essere utilizzati come se fossero l'oggetto originale, e non devono essere chiamati prima che l'oggetto sia accessibile. Il che vuol dire che essi possono essere passati a una libreria che non sa se sta ricevendo un riferimento in luogo dell'oggetto reale.
# weakref_proxy.py
import weakref
class ExpensiveObject:
def __init__(self, name):
self.name = name
def __del__(self):
print('(Eliminazione di {})'.format(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
.
$ python3 weakref_proxy.py via obj: Il mio oggetto via ref: Il mio oggetto via proxy: Il mio oggetto (Eliminazione di <__main__.ExpensiveObject object at 0x7f0f9a549978>) Traceback (most recent call last): File "weakref_proxy.py", line 23, in <module> print('via proxy:', p.name) ReferenceError: weakly-referenced object no longer exists
Cache degli Oggetti
Le classi ref
e proxy
sono considerate classi di "basso livello". Laddove esse sono utili per mantenere riferimenti deboli a oggetti individuali e per consentire a cicli di essere raccolti dal garbage collector, le classi WeakKeyDictionary
e WeakValueDictionary
forniscono una API più appropriata per creare una cache di parecchi oggetti.
La classe WeakValueDictionary
utilizza riferimenti deboli ai valori che conserva, consentendo di essere poi raccolti dal garbage collector quando non vengono più utilizzati da altre parti di codice. Usando chiamate esplicite al garbage collector si dimostra la differenza tra la gestione della memoria con un normale dizionario e con WeakValueDictionary
.
# weakref_valuedict.py
import gc
from pprint import pprint
import weakref
gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
class ExpensiveObject(object):
def __init__(self, name):
self.name = name
def __repr__(self):
return 'ExpensiveObject(%s)' % self.name
def __del__(self):
print(' (In eliminazione {})'.format(self))
def demo(cache_factory):
# trattiene gli oggetti in modo che nessuna weak reference
# venga rimossa immediatamente
all_refs = {}
# creazione della cache utilizzando la factory
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 =', end=' ')
pprint(all_refs)
print('\n Prima, la cache contiene:', list(cache.keys()))
for name, value in cache.items():
print(' {} = {}'.format(name, value))
del value # decref
# Rimuove tutti i riferimenti agli oggetti tranne la cache
print('\n Pulizia:')
del all_refs
gc.collect()
print('\n Dopo, la cache contiene:', list(cache.keys()))
for name, value in cache.items():
print(' {} = {}'.format(name, value))
print(' demo in uscita')
return
demo(dict)
print
demo(weakref.WeakValueDictionary)
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.
$ python3 weakref_valuedict.py TIPO CACHE: <class 'dict'> all_refs = {'due': ExpensiveObject(due), 'tre': ExpensiveObject(tre), 'uno': ExpensiveObject(uno)} Prima, la cache contiene: ['due', 'tre', 'uno'] due = ExpensiveObject(due) tre = ExpensiveObject(tre) uno = ExpensiveObject(uno) Pulizia: Dopo, la cache contiene: ['due', 'tre', 'uno'] due = ExpensiveObject(due) tre = ExpensiveObject(tre) uno = ExpensiveObject(uno) demo in uscita (In eliminazione ExpensiveObject(due)) (In eliminazione ExpensiveObject(tre)) (In eliminazione ExpensiveObject(uno)) TIPO CACHE: <class 'weakref.WeakValueDictionary'> all_refs = {'due': ExpensiveObject(due), 'tre': ExpensiveObject(tre), 'uno': ExpensiveObject(uno)} Prima, la cache contiene: ['due', 'tre', 'uno'] due = ExpensiveObject(due) tre = ExpensiveObject(tre) uno = ExpensiveObject(uno) Pulizia: (In eliminazione ExpensiveObject(due)) (In eliminazione ExpensiveObject(tre)) (In eliminazione 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.
Cautela: Visto un un
WeakValueDictionary
è costruito sopra un dizionario Python, non deve mutare dimensione quando ci si itera sopra. Il che può essere difficile assicurare per un WeakValueDictionary
visto che le azioni eseguite dal programma durante l'iterazione possono fare sì che gli elementi nel dizionario scompaiano "magicamente" (come effetto collaterale della raccolta del garbage collector)