weakref — Referencias no-permanentes a objetos

Propósito:Referencia a un objeto «costoso», pero permite que su memoria sea reclamada por el recolector de basura si no hay otras referencias no débiles.

El módulo weakref admite referencias débiles a objetos. Una referencia normal incrementa el conteo de referencias en el objeto y previene que sea recolectado. Este resultado no siempre es deseable, especialmente cuando una referencia circular podría estar presente o cuando un caché de objetos se deben eliminar cuando se necesita memoria. Una referencia débil es un identificador para un objeto que no evita que se lo recolecte automáticamente.

Referencias

Las referencias débiles a objetos se gestionan a través de la clase ref. Para recuperar el objeto original, llama al objeto de referencia.

weakref_ref.py
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))


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

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

print('deleting obj')
del obj
print('r():', r())

En este caso, dado que obj se borra antes de la segunda llamada a la referencia, el ref devuelve None

$ python3 weakref_ref.py

obj: <__main__.ExpensiveObject object at 0x1007b1a58>
ref: <weakref at 0x1007a92c8; to 'ExpensiveObject' at
0x1007b1a58>
r(): <__main__.ExpensiveObject object at 0x1007b1a58>
deleting obj
(Deleting <__main__.ExpensiveObject object at 0x1007b1a58>)
r(): None

Devolución de llamada de referencias

El constructor ref acepta una función de devolución de llamada opcional que es invocada cuando se borra el objeto al que se hace referencia.

weakref_ref_callback.py
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))


def callback(reference):
    """Invoked when referenced object is deleted"""
    print('callback({!r})'.format(reference))


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

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

print('deleting obj')
del obj
print('r():', r())

La devolución de llamada recibe el objeto de referencia como un argumento después de que la referencia está «muerta» y ya no se refiere al objeto original. Un uso de esta función es eliminar el objeto de referencia débil de un caché.

$ python3 weakref_ref_callback.py

obj: <__main__.ExpensiveObject object at 0x1010b1978>
ref: <weakref at 0x1010a92c8; to 'ExpensiveObject' at
0x1010b1978>
r(): <__main__.ExpensiveObject object at 0x1010b1978>
deleting obj
(Deleting <__main__.ExpensiveObject object at 0x1010b1978>)
callback(<weakref at 0x1010a92c8; dead>)
r(): None

Finalizando Objetos

Para una gestión más robusta de los recursos cuando las referencias débiles son eliminadas, usa finalize para asociar devoluciones de llamadas con objetos. Una instancia finalize se conserva hasta que se elimina el objeto adjunto, incluso si la aplicación no conserva una referencia al finalizador

weakref_finalize.py
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))


def on_finalize(*args):
    print('on_finalize({!r})'.format(args))


obj = ExpensiveObject()
weakref.finalize(obj, on_finalize, 'extra argument')

del obj

Los argumentos para finalize son el objeto a seguir, una llamada para ser invocada cuando el objeto es recolectado, y cualquier argumento posicional o con nombre para pasar al invocable.

$ python3 weakref_finalize.py

(Deleting <__main__.ExpensiveObject object at 0x1019b10f0>)
on_finalize(('extra argument',))

La instancia finalize tiene una propiedad atexit que controla si la devolución de llamada se invoca cuando un programa está saliendo, si aún no ha sido llamado.

weakref_finalize_atexit.py
import sys
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.format(self))


def on_finalize(*args):
    print('on_finalize({!r})'.format(args))


obj = ExpensiveObject()
f = weakref.finalize(obj, on_finalize, 'extra argument')
f.atexit = bool(int(sys.argv[1]))

El valor predeterminado es invocar la devolución de llamada. Configurando atexit a falso deshabilita ese comportamiento.

$ python3 weakref_finalize_atexit.py 1

on_finalize(('extra argument',))
(Deleting <__main__.ExpensiveObject object at 0x1007b10f0>)

$ python3 weakref_finalize_atexit.py 0

Dando a la instancia finalize una referencia al objeto que rastrea hace que se retenga una referencia, por lo que el objeto nunca es recolectado.

weakref_finalize_reference.py
import gc
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.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('found uncollected object in gc')

Como muestra este ejemplo, a pesar de que la referencia explícita a obj se elimina, el objeto se conserva y es visible para el recolector de basura a través de f.

$ python3 weakref_finalize_reference.py

found uncollected object in gc

El uso de un método vinculado de un objeto rastreado como el que se puede llamar también puede evitar que un objeto se finalice correctamente.

weakref_finalize_reference_method.py
import gc
import weakref


class ExpensiveObject:

    def __del__(self):
        print('(Deleting {})'.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('found uncollected object in gc')

Porque el invocable dado a finalize es un método vinculado a la instancia obj, el objeto finalizado contiene una referencia a obj, que no se puede eliminar y recolectar.

$ python3 weakref_finalize_reference_method.py

found uncollected object in gc

Proxies

A veces es más conveniente usar un proxy, en lugar de un referencia débil. Los proxies se pueden usar como si fueran objetos originales, y no necesitan ser llamados antes de que el objeto sea accesible. Como consecuencia, pueden pasarse a una biblioteca que no sabe que está recibiendo una referencia en lugar del objeto real.

weakref_proxy.py
import weakref


class ExpensiveObject:

    def __init__(self, name):
        self.name = name

    def __del__(self):
        print('(Deleting {})'.format(self))


obj = ExpensiveObject('My Object')
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)

Si se accede al proxy después de eliminar el objeto referente, se eleva la excepción ReferenceError.

$ python3 weakref_proxy.py

via obj: My Object
via ref: My Object
via proxy: My Object
(Deleting <__main__.ExpensiveObject object at 0x1007aa7b8>)
Traceback (most recent call last):
  File "weakref_proxy.py", line 30, in <module>
    print('via proxy:', p.name)
ReferenceError: weakly-referenced object no longer exists

Almacenamiento en caché de objetos

Las clases ref y proxy se consideran de «bajo nivel». Si bien son útiles para mantener las referencias débiles a objetos individuales y permitiendo que ciclos sean recolectados, Las clases WeakKeyDictionary y WeakValueDictionary proporcionan una interfaz más apropiada para crear un caché de varios objetos.

La clase WeakValueDictionary usa referencias débiles a los valores que contiene, lo que permite que sean recolectadoscuando el otro código no está realmente usándolos. Usar llamadas explícitas al recolector de basura ilustra la diferencia entre el manejo de la memoria con un diccionario y WeakValueDictionary:

weakref_valuedict.py
import gc
from pprint import pprint
import weakref

gc.set_debug(gc.DEBUG_UNCOLLECTABLE)


class ExpensiveObject:

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return 'ExpensiveObject({})'.format(self.name)

    def __del__(self):
        print('    (Deleting {})'.format(self))


def demo(cache_factory):
    # hold objects so any weak references
    # are not removed immediately
    all_refs = {}
    # create the cache using the factory
    print('CACHE TYPE:', cache_factory)
    cache = cache_factory()
    for name in ['one', 'two', 'three']:
        o = ExpensiveObject(name)
        cache[name] = o
        all_refs[name] = o
        del o  # decref

    print('  all_refs =', end=' ')
    pprint(all_refs)
    print('\n  Before, cache contains:', list(cache.keys()))
    for name, value in cache.items():
        print('    {} = {}'.format(name, value))
        del value  # decref

    # remove all references to the objects except the cache
    print('\n  Cleanup:')
    del all_refs
    gc.collect()

    print('\n  After, cache contains:', list(cache.keys()))
    for name, value in cache.items():
        print('    {} = {}'.format(name, value))
    print('  demo returning')
    return


demo(dict)
print()

demo(weakref.WeakValueDictionary)

Cualquier variable de bucle que haga referencia a los valores almacenados en caché debe ser limpiada explícitamente por lo que el recuento de referencia del objeto es decrementado. De lo contrario, el recolector de basura no eliminará los objetos y éstos permanecerán en el caché. De manera similar, la variable all_refs se usa para mantener referencias para evitar que sean recolectados prematuramente.

$ python3 weakref_valuedict.py

CACHE TYPE: <class 'dict'>
  all_refs = {'one': ExpensiveObject(one),
 'three': ExpensiveObject(three),
 'two': ExpensiveObject(two)}

  Before, cache contains: ['one', 'three', 'two']
    one = ExpensiveObject(one)
    three = ExpensiveObject(three)
    two = ExpensiveObject(two)

  Cleanup:

  After, cache contains: ['one', 'three', 'two']
    one = ExpensiveObject(one)
    three = ExpensiveObject(three)
    two = ExpensiveObject(two)
  demo returning
    (Deleting ExpensiveObject(one))
    (Deleting ExpensiveObject(three))
    (Deleting ExpensiveObject(two))

CACHE TYPE: <class 'weakref.WeakValueDictionary'>
  all_refs = {'one': ExpensiveObject(one),
 'three': ExpensiveObject(three),
 'two': ExpensiveObject(two)}

  Before, cache contains: ['one', 'three', 'two']
    one = ExpensiveObject(one)
    three = ExpensiveObject(three)
    two = ExpensiveObject(two)

  Cleanup:
    (Deleting ExpensiveObject(one))
    (Deleting ExpensiveObject(three))
    (Deleting ExpensiveObject(two))

  After, cache contains: []
  demo returning

El WeakKeyDictionary funciona de manera similar pero usa referencias débiles para las llaves en lugar de los valores en el diccionario.

Advertencia

La documentación de la biblioteca para weakref contiene esta advertencia:

Precaución: Porque un WeakValueDictionary se construye sobre un diccionario de Python, no debe cambiar el tamaño al iterar sobre éste. Esto puede ser difícil de asegurar para un WeakValueDictionary porque las acciones realizadas por el programa durante la iteración pueden hacer que los elementos en el diccionario desaparezcan «por magia» (como efecto secundario de la recolección de basura).

Ver también