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.

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 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.

La documentazione standard per weakref contiene questo avvertimento:

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)

Vedere anche:

weakref
La documentazione della libreria standard per questo modulo.
gc
Il modulo gc è l'interfaccia al garbage collector dell'interprete.
PEP 205
Proposta di miglioramento per i riferimenti deboli (in inglese)