gc — Recolector de basura

Propósito:Administra la memoria utilizada por los objetos de Python

gc expone el mecanismo de administración de memoria subyacente de Python, el recolector de basura automático. El módulo incluye funciones para controlar cómo funciona el recolector y para examinar los objetos conocidos por el sistema, ya sea pendientes de recolección o atascados en ciclos de referencia y que no se pueden liberar.

Rastreo de referencias

Con gc, las referencias entrantes y salientes entre objetos se pueden usar para encontrar ciclos en estructuras de datos complejas. Si se sabe que una estructura de datos tiene un ciclo, se puede usar un código personalizado para examinar sus propiedades. Si el ciclo está en código desconocido, las funciones get_referents() y get_referrers() se pueden usar para construir herramientas genéricas de depuración.

Por ejemplo, get_referents() muestra los objetos referidos por los argumentos de entrada.

gc_get_referents.py
import gc
import pprint


class Graph:

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

    def set_next(self, next):
        print('Linking nodes {}.next = {}'.format(self, next))
        self.next = next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


# Construct a graph cycle
one = Graph('one')
two = Graph('two')
three = Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)

print()
print('three refers to:')
for r in gc.get_referents(three):
    pprint.pprint(r)

En este caso, la instancia Graph three contiene referencias a su diccionario de instancias (en el atributo __dict__) y su clase.

$ python3 gc_get_referents.py

Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)

three refers to:
{'name': 'three', 'next': Graph(one)}
<class '__main__.Graph'>

El siguiente ejemplo utiliza una Queue para realizar un recorrido transversal de todas las referencias de objetos en busca de ciclos. Los elementos insertados en la cola son tuplas que contienen la cadena de referencia hasta ahora y el siguiente objeto a examinar. Comienza con three y analiza todo a lo que se refiere. Omitir clases evita mirar métodos, módulos, etc.

gc_get_referents_cycles.py
import gc
import pprint
import queue


class Graph:

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

    def set_next(self, next):
        print('Linking nodes {}.next = {}'.format(self, next))
        self.next = next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


# Construct a graph cycle
one = Graph('one')
two = Graph('two')
three = Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)

print()

seen = set()
to_process = queue.Queue()

# Start with an empty object chain and Graph three.
to_process.put(([], three))

# Look for cycles, building the object chain for each object
# found in the queue so the full cycle can be printed at the
# end.
while not to_process.empty():
    chain, next = to_process.get()
    chain = chain[:]
    chain.append(next)
    print('Examining:', repr(next))
    seen.add(id(next))
    for r in gc.get_referents(next):
        if isinstance(r, str) or isinstance(r, type):
            # Ignore strings and classes
            pass
        elif id(r) in seen:
            print()
            print('Found a cycle to {}:'.format(r))
            for i, link in enumerate(chain):
                print('  {}: '.format(i), end=' ')
                pprint.pprint(link)
        else:
            to_process.put((chain, r))

El ciclo en los nodos se encuentra fácilmente al observar los objetos que ya han sido procesados. Para evitar contener referencias a esos objetos, sus valores id() se almacenan en caché en un conjunto. Los objetos del diccionario que se encuentran en el ciclo son los valores __dict__ para las instancias Graph, y contienen sus atributos de instancia.

$ python3 gc_get_referents_cycles.py

Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)

Examining: Graph(three)
Examining: {'name': 'three', 'next': Graph(one)}
Examining: Graph(one)
Examining: {'name': 'one', 'next': Graph(two)}
Examining: Graph(two)
Examining: {'name': 'two', 'next': Graph(three)}

Found a cycle to Graph(three):
  0:  Graph(three)
  1:  {'name': 'three', 'next': Graph(one)}
  2:  Graph(one)
  3:  {'name': 'one', 'next': Graph(two)}
  4:  Graph(two)
  5:  {'name': 'two', 'next': Graph(three)}

Forzar la recolección de basura

Aunque el recolector de basura se ejecuta automáticamente cuando el intérprete ejecuta un programa, se puede activar para que se ejecute en un momento específico cuando hay muchos objetos para liberar o no hay mucho trabajo y el recolector no afectará el rendimiento de la aplicación. Activa la recolección usando collect().

gc_collect.py
import gc
import pprint


class Graph:

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

    def set_next(self, next):
        print('Linking nodes {}.next = {}'.format(self, next))
        self.next = next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


# Construct a graph cycle
one = Graph('one')
two = Graph('two')
three = Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)

# Remove references to the graph nodes in this module's namespace
one = two = three = None

# Show the effect of garbage collection
for i in range(2):
    print('\nCollecting {} ...'.format(i))
    n = gc.collect()
    print('Unreachable objects:', n)
    print('Remaining Garbage:', end=' ')
    pprint.pprint(gc.garbage)

En este ejemplo, el ciclo se borra tan pronto como la recolección se ejecute por primera vez, ya que nada se refiere a los nodos Graph excepto ellos mismos. collect() devuelve el número de objetos «inalcanzables» que encontró. En este caso, el valor es 6 porque hay tres objetos con sus diccionarios de atributos de instancia.

$ python3 gc_collect.py

Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)

Collecting 0 ...
Unreachable objects: 6
Remaining Garbage: []

Collecting 1 ...
Unreachable objects: 0
Remaining Garbage: []

Encontrar referencias a objetos que no se pueden recolectar

Buscar el objeto que contiene una referencia a otro objeto es un poco más complicado que ver a qué hace referencia un objeto. Debido a que el código que pregunta por la referencia debe contener una referencia, algunos de los referentes deben ser ignorados. Este ejemplo crea un ciclo gráfico, luego trabaja a través de las instancias Graph y elimina la referencia en el nodo «padre».

gc_get_referrers.py
import gc
import pprint


class Graph:

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

    def set_next(self, next):
        print('Linking nodes {}.next = {}'.format(self, next))
        self.next = next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)

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


# Construct a graph cycle
one = Graph('one')
two = Graph('two')
three = Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)

# Collecting now keeps the objects as uncollectable,
# but not garbage.
print()
print('Collecting...')
n = gc.collect()
print('Unreachable objects:', n)
print('Remaining Garbage:', end=' ')
pprint.pprint(gc.garbage)

# Ignore references from local variables in this module, global
# variables, and from the garbage collector's bookkeeping.
REFERRERS_TO_IGNORE = [locals(), globals(), gc.garbage]


def find_referring_graphs(obj):
    print('Looking for references to {!r}'.format(obj))
    referrers = (r for r in gc.get_referrers(obj)
                 if r not in REFERRERS_TO_IGNORE)
    for ref in referrers:
        if isinstance(ref, Graph):
            # A graph node
            yield ref
        elif isinstance(ref, dict):
            # An instance or other namespace dictionary
            for parent in find_referring_graphs(ref):
                yield parent


# Look for objects that refer to the objects in the graph.
print()
print('Clearing referrers:')
for obj in [one, two, three]:
    for ref in find_referring_graphs(obj):
        print('Found referrer:', ref)
        ref.set_next(None)
        del ref  # remove reference so the node can be deleted
    del obj  # remove reference so the node can be deleted

# Clear references held by gc.garbage
print()
print('Clearing gc.garbage:')
del gc.garbage[:]

# Everything should have been freed this time
print()
print('Collecting...')
n = gc.collect()
print('Unreachable objects:', n)
print('Remaining Garbage:', end=' ')
pprint.pprint(gc.garbage)

Este tipo de lógica es exagerada si se entienden los ciclos, pero para un ciclo inexplicable en los datos usando get_referrers() puede exponer la relación inesperada.

$ python3 gc_get_referrers.py

Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)

Collecting...
Unreachable objects: 0
Remaining Garbage: []

Clearing referrers:
Looking for references to Graph(one)
Looking for references to {'name': 'three', 'next': Graph(one)}
Found referrer: Graph(three)
Linking nodes Graph(three).next = None
Looking for references to Graph(two)
Looking for references to {'name': 'one', 'next': Graph(two)}
Found referrer: Graph(one)
Linking nodes Graph(one).next = None
Looking for references to Graph(three)
Looking for references to {'name': 'two', 'next': Graph(three)}
Found referrer: Graph(two)
Linking nodes Graph(two).next = None

Clearing gc.garbage:

Collecting...
Unreachable objects: 0
Remaining Garbage: []
Graph(one).__del__()
Graph(two).__del__()
Graph(three).__del__()

Umbrales de recolección y generaciones

El recolector de basura mantiene tres listas de objetos que ve mientras se ejecuta, uno para cada «generación» rastreada por el recolector. A medida que se examinan los objetos en cada generación, se recogen o envejecen en generaciones posteriores hasta que finalmente alcanzan la etapa en la que se mantienen permanentemente.

Las rutinas del recolector pueden ajustarse para que ocurran a diferentes frecuencias en función de la diferencia entre el número de asignaciones de objetos y las desasignaciones entre ejecuciones. Cuando el número de asignaciones menos el número de desasignaciones es mayor que el umbral para la generación, se ejecuta el recolector de basura. Los umbrales actuales se pueden examinar con get_threshold().

gc_get_threshold.py
import gc

print(gc.get_threshold())

El valor de retorno es una tupla con el umbral para cada generación.

$ python3 gc_get_threshold.py

(700, 10, 10)

Los umbrales se pueden cambiar con set_threshold(). Este programa de ejemplo usa un argumento de línea de comando para establecer el umbral para la generación 0 y luego asigna una serie de objetos.

gc_threshold.py
import gc
import pprint
import sys

try:
    threshold = int(sys.argv[1])
except (IndexError, ValueError, TypeError):
    print('Missing or invalid threshold, using default')
    threshold = 5


class MyObj:

    def __init__(self, name):
        self.name = name
        print('Created', self.name)


gc.set_debug(gc.DEBUG_STATS)

gc.set_threshold(threshold, 1, 1)
print('Thresholds:', gc.get_threshold())

print('Clear the collector by forcing a run')
gc.collect()
print()

print('Creating objects')
objs = []
for i in range(10):
    objs.append(MyObj(i))
print('Exiting')

# Turn off debugging
gc.set_debug(0)

Los diferentes valores de umbral introducen los barridos de recolección de basura en diferentes momentos, que se muestran aquí porque la depuración está habilitada.

$ python3 -u gc_threshold.py 5

Thresholds: (5, 1, 1)
Clear the collector by forcing a run
gc: collecting generation 2...
gc: objects in each generation: 505 2161 4858
gc: done, 0.0010s elapsed

Creating objects
gc: collecting generation 0...
gc: objects in each generation: 5 0 7323
gc: done, 0.0000s elapsed
Created 0
Created 1
gc: collecting generation 0...
gc: objects in each generation: 4 2 7323
gc: done, 0.0000s elapsed
Created 2
Created 3
Created 4
gc: collecting generation 1...
gc: objects in each generation: 6 3 7323
gc: done, 0.0000s elapsed
Created 5
Created 6
Created 7
gc: collecting generation 0...
gc: objects in each generation: 6 0 7329
gc: done, 0.0000s elapsed
Created 8
Created 9
Exiting

Un umbral más pequeño hace que los barridos se ejecuten con más frecuencia.

$ python3 -u gc_threshold.py 2

Thresholds: (2, 1, 1)
Clear the collector by forcing a run
gc: collecting generation 2...
gc: objects in each generation: 505 2161 4858
gc: done, 0.0010s elapsed
gc: collecting generation 0...
gc: objects in each generation: 2 0 7323
gc: done, 0.0000s elapsed

Creating objects
gc: collecting generation 0...
gc: objects in each generation: 5 0 7323
gc: done, 0.0000s elapsed
gc: collecting generation 1...
gc: objects in each generation: 3 3 7323
gc: done, 0.0000s elapsed
Created 0
Created 1
gc: collecting generation 0...
gc: objects in each generation: 4 0 7325
gc: done, 0.0000s elapsed
Created 2
gc: collecting generation 0...
gc: objects in each generation: 7 1 7325
gc: done, 0.0000s elapsed
Created 3
Created 4
gc: collecting generation 1...
gc: objects in each generation: 4 3 7325
gc: done, 0.0000s elapsed
Created 5
gc: collecting generation 0...
gc: objects in each generation: 7 0 7329
gc: done, 0.0000s elapsed
Created 6
Created 7
gc: collecting generation 0...
gc: objects in each generation: 4 2 7329
gc: done, 0.0000s elapsed
Created 8
gc: collecting generation 1...
gc: objects in each generation: 7 3 7329
gc: done, 0.0000s elapsed
Created 9
Exiting

Depuración

La depuración de pérdidas de memoria puede ser un desafío. gc incluye varias opciones para exponer el funcionamiento interno para facilitar el trabajo. Las opciones son máscaras de bits destinadas a combinarse y pasarse a set_debug() para configurar el recolector de basura mientras se ejecuta el programa. La información de depuración se imprime en sys.stderr.

El indicador DEBUG_STATS activa el informe de estadísticas, lo que hace que el recolector de basura informe cuando se está ejecutando, la cantidad de objetos rastreados para cada generación y la cantidad de tiempo que llevó realizar el barrido.

gc_debug_stats.py
import gc

gc.set_debug(gc.DEBUG_STATS)

gc.collect()
print('Exiting')

Este resultado de ejemplo muestra dos ejecuciones separadas del recolector porque se ejecuta una vez cuando se invoca explícitamente y una segunda vez cuando termina el intérprete.

$ python3 gc_debug_stats.py

gc: collecting generation 2...
gc: objects in each generation: 618 1413 4860
gc: done, 0.0009s elapsed
Exiting
gc: collecting generation 2...
gc: objects in each generation: 1 0 6746
gc: done, 0.0022s elapsed
gc: collecting generation 2...
gc: objects in each generation: 113 0 6570
gc: done, 2930 unreachable, 0 uncollectable, 0.0012s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 3189
gc: done, 151 unreachable, 0 uncollectable, 0.0003s elapsed

Habilitar DEBUG_COLLECTABLE y DEBUG_UNCOLLECTABLE hace que el recolector informe si cada objeto que examina puede o no ser recolectado. Si ver los objetos que no se pueden recolectar no es suficiente información para comprender dónde se retienen los datos, habilita DEBUG_SAVEALL para que gc conserve todos los objetos que encuentre sin ninguna referencia en la lista garbage.

gc_debug_saveall.py
import gc

flags = (gc.DEBUG_COLLECTABLE |
         gc.DEBUG_UNCOLLECTABLE |
         gc.DEBUG_SAVEALL
         )

gc.set_debug(flags)


class Graph:

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

    def set_next(self, next):
        self.next = next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


class CleanupGraph(Graph):

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


# Construct a graph cycle
one = Graph('one')
two = Graph('two')
one.set_next(two)
two.set_next(one)

# Construct another node that stands on its own
three = CleanupGraph('three')

# Construct a graph cycle with a finalizer
four = CleanupGraph('four')
five = CleanupGraph('five')
four.set_next(five)
five.set_next(four)

# Remove references to the graph nodes in this module's namespace
one = two = three = four = five = None

# Force a sweep
print('Collecting')
gc.collect()
print('Done')

# Report on what was left
for o in gc.garbage:
    if isinstance(o, Graph):
        print('Retained: {} 0x{:x}'.format(o, id(o)))

# Reset the debug flags before exiting to avoid dumping a lot
# of extra information and making the example output more
# confusing.
gc.set_debug(0)

Esto permite que los objetos se examinen después de la recolección de basura, lo que es útil si, por ejemplo, el constructor no se puede cambiar para imprimir la identificación del objeto cuando se crea cada objeto.

$ python3 -u gc_debug_saveall.py

CleanupGraph(three).__del__()
Collecting
gc: collectable <Graph 0x101fe1f28>
gc: collectable <Graph 0x103d02048>
gc: collectable <dict 0x101c92678>
gc: collectable <dict 0x101c926c0>
gc: collectable <CleanupGraph 0x103d02160>
gc: collectable <CleanupGraph 0x103d02198>
gc: collectable <dict 0x101fe73f0>
gc: collectable <dict 0x101fe7360>
CleanupGraph(four).__del__()
CleanupGraph(five).__del__()
Done
Retained: Graph(one) 0x101fe1f28
Retained: Graph(two) 0x103d02048
Retained: CleanupGraph(four) 0x103d02160
Retained: CleanupGraph(five) 0x103d02198

Para simplificar, DEBUG_LEAK se define como una combinación de todas las otras opciones.

gc_debug_leak.py
import gc

flags = gc.DEBUG_LEAK

gc.set_debug(flags)


class Graph:

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

    def set_next(self, next):
        self.next = next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


class CleanupGraph(Graph):

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


# Construct a graph cycle
one = Graph('one')
two = Graph('two')
one.set_next(two)
two.set_next(one)

# Construct another node that stands on its own
three = CleanupGraph('three')

# Construct a graph cycle with a finalizer
four = CleanupGraph('four')
five = CleanupGraph('five')
four.set_next(five)
five.set_next(four)

# Remove references to the graph nodes in this module's namespace
one = two = three = four = five = None

# Force a sweep
print('Collecting')
gc.collect()
print('Done')

# Report on what was left
for o in gc.garbage:
    if isinstance(o, Graph):
        print('Retained: {} 0x{:x}'.format(o, id(o)))

# Reset the debug flags before exiting to avoid dumping a lot
# of extra information and making the example output more
# confusing.
gc.set_debug(0)

Ten en cuenta que debido a que DEBUG_SAVEALL es habilitado por DEBUG_LEAK, incluso los objetos sin referencia que normalmente se habrían recolectado y eliminado se conservan.

$ python3 -u gc_debug_leak.py

CleanupGraph(three).__del__()
Collecting
gc: collectable <Graph 0x1044e1f28>
gc: collectable <Graph 0x1044eb048>
gc: collectable <dict 0x101c92678>
gc: collectable <dict 0x101c926c0>
gc: collectable <CleanupGraph 0x1044eb160>
gc: collectable <CleanupGraph 0x1044eb198>
gc: collectable <dict 0x1044e7360>
gc: collectable <dict 0x1044e72d0>
CleanupGraph(four).__del__()
CleanupGraph(five).__del__()
Done
Retained: Graph(one) 0x1044e1f28
Retained: Graph(two) 0x1044eb048
Retained: CleanupGraph(four) 0x1044eb160
Retained: CleanupGraph(five) 0x1044eb198

Ver también