En general, no necesita preocuparse por el recolector de basura y trabajar con la memoria cuando escribe código Python. Tan pronto como los objetos ya no sean necesarios, Python libera automáticamente la memoria debajo de ellos. A pesar de esto, comprender cómo funciona GC lo ayudará a escribir un mejor código.
Administrador de memoria
A diferencia de otros lenguajes populares, Python no libera toda la memoria al sistema operativo tan pronto como elimina un objeto. En su lugar, utiliza un administrador de memoria adicional diseñado para objetos pequeños (cuyo tamaño es inferior a 512 bytes). Para trabajar con tales objetos, asigna grandes bloques de memoria, en los que se almacenarán muchos objetos pequeños en el futuro.
Tan pronto como se elimina uno de los objetos pequeños, la memoria de debajo no va al sistema operativo, Python lo deja para nuevos objetos con el mismo tamaño. Si no quedan objetos en uno de los bloques de memoria asignados, Python puede liberarlo al sistema operativo. Por lo general, los bloques se liberan cuando el script crea muchos objetos temporales.
Por lo tanto, si un proceso de Python de larga duración comienza a consumir más memoria con el tiempo, esto no significa en absoluto que su código tenga un problema de pérdida de memoria. Si quieres saber más sobre el administrador de memoria en Python, puedes leerlo en
mi otro artículo .
Algoritmos de Recolección de Basura
El intérprete de Python estándar (CPython) utiliza dos algoritmos a la vez, el recuento de referencias y el recolector de basura generacional (en adelante GC), mejor conocido como el
módulo gc estándar de Python.
El algoritmo de conteo de enlaces es muy simple y eficiente, pero tiene un gran inconveniente. No sabe cómo definir referencias circulares. Es por esto que en Python hay un colector adicional llamado GC generacional que monitorea objetos con posibles referencias circulares.
En Python, el algoritmo de conteo de referencia es fundamental y no se puede deshabilitar, mientras que el GC es opcional y se puede deshabilitar.
Algoritmo de conteo de enlaces
El algoritmo de conteo de enlaces es una de las técnicas de recolección de basura más fáciles. Los objetos se eliminan tan pronto como ya no se hace referencia a ellos.
En Python, las variables no almacenan valores, sino que actúan como referencias a objetos. Es decir, cuando asigna un valor a una nueva variable, primero se crea un objeto con este valor, y solo entonces la variable comienza a referirse a él. Varias variables pueden hacer referencia a un solo objeto.
Cada objeto en Python contiene un campo adicional (contador de referencia), que almacena el número de enlaces a él. Tan pronto como alguien se refiere a un objeto, este campo se incrementa en uno. Si por alguna razón el enlace desaparece, este campo se reduce en uno.
Ejemplos cuando aumenta el número de enlaces:
- operador de asignación
- pasar argumentos
- inserte un nuevo objeto en la hoja (el número de enlaces para el objeto aumenta)
- una construcción de la forma foo = bar (foo comienza a referirse al mismo objeto que bar)
Tan pronto como el contador de referencia para un objeto específico llega a cero, el intérprete comienza el proceso de destruir el objeto. Si el objeto remoto contenía enlaces a otros objetos, estos enlaces también se eliminan. Por lo tanto, la eliminación de un objeto puede implicar la eliminación de otros.
Por ejemplo, si se elimina una lista, el recuento de referencias en todos sus elementos se reduce en uno. Si todos los objetos de la lista no se usan en ningún otro lugar, también se eliminarán.
Las variables que se declaran fuera de las funciones, clases y bloques se denominan globales. Típicamente, el ciclo de vida de tales variables es igual a la vida del proceso de Python. Por lo tanto, el número de referencias a objetos referenciados por variables globales nunca cae a cero.
Las variables que se declaran dentro de un bloque (función, clase) tienen visibilidad local (es decir, solo son visibles dentro del bloque). Tan pronto como el intérprete de Python sale del bloque, destruye todos los enlaces creados por las variables locales dentro de él.
Siempre puede verificar el número de enlaces utilizando la función
sys.getrefcount
.
Ejemplo de un contador de enlaces:
foo = []
La razón principal por la que el intérprete estándar (CPython) usa un contador de referencia es histórica. Actualmente hay mucho debate sobre este enfoque. Algunas personas creen que un recolector de basura puede ser mucho más eficiente sin un algoritmo de conteo de enlaces. Este algoritmo tiene muchos problemas, como enlaces circulares, subprocesos de bloqueo, así como sobrecarga adicional para memoria y CPU.
La principal ventaja de este algoritmo es que los objetos se eliminan inmediatamente tan pronto como no se necesitan.
Recolector de basura opcional.
¿Por qué necesitamos un algoritmo adicional cuando ya tenemos un recuento de referencias?
Desafortunadamente, el algoritmo clásico de conteo de enlaces tiene un gran inconveniente: no sabe cómo encontrar enlaces circulares. Los bucles se producen cuando uno o más objetos se refieren entre sí.
Dos ejemplos:

Como puede ver, el objeto
object1
refiere a sí mismo, mientras que
object1
y
object2
refieren entre sí. Para tales objetos, el recuento de referencia siempre será 1.
Demo de Python:
import gc
En el ejemplo anterior, la instrucción del elimina las referencias a nuestros objetos (no a los objetos mismos). Una vez que Python ejecuta una declaración del, estos objetos se vuelven inaccesibles desde el código Python. Sin embargo, con el módulo gc apagado, permanecerán en la memoria, ya que tenían referencias circulares y su contador sigue siendo uno. Puede explorar visualmente tales relaciones utilizando la biblioteca
objgraph
.
Para solucionar este problema, se agregó un algoritmo adicional conocido como
módulo gc en Python 1.5. La única tarea es la eliminación de objetos circulares a los que ya no hay acceso desde el código.
Los bucles de retorno solo pueden ocurrir en objetos "contenedor". Es decir en objetos que pueden almacenar otros objetos, como listas, diccionarios, clases y tuplas. GC no realiza un seguimiento de los tipos simples e inmutables, excepto las tuplas. Algunas tuplas y diccionarios también se excluyen de la lista de seguimiento cuando se cumplen ciertas condiciones. Con todos los demás objetos, el algoritmo de conteo de referencia está garantizado para hacer frente.
Cuando se activa el GC
A diferencia del algoritmo de conteo de referencia, el GC cíclico no funciona en tiempo real y se ejecuta periódicamente. Cada ejecución del recolector crea micropausas en el código, por lo que CPython (el intérprete estándar) utiliza varias heurísticas para determinar la frecuencia del recolector de basura.
El recolector de basura cíclico divide todos los objetos en 3 generaciones (generaciones). Los nuevos objetos caen en la primera generación. Si la nueva instalación sobrevive al proceso de recolección de basura, entonces pasa a la próxima generación. Cuanto más alta es la generación, con menos frecuencia se escanea en busca de basura. Dado que los objetos nuevos a menudo tienen una vida útil muy corta (son temporales), tiene sentido entrevistarlos con más frecuencia que aquellos que ya han pasado por varias etapas de recolección de basura.
Cada generación tiene un contador especial y un umbral de respuesta, al alcanzar el cual se desencadena el proceso de recolección de basura. Cada contador almacena el número de asignaciones menos el número de desasignaciones en una generación dada. Tan pronto como se crea cualquier objeto contenedor en Python, verifica estos contadores. Si las condiciones funcionan, entonces comienza el proceso de recolección de basura.
Si varias o más generaciones han cruzado el umbral a la vez, se selecciona la generación más senior. Esto se debe al hecho de que las generaciones anteriores también escanean todas las anteriores. Para reducir el número de pausas de recolección de basura para objetos de larga vida, la generación más antigua tiene
un conjunto adicional de condiciones .
Los umbrales estándar para generaciones se establecen en
700, 10 10
respectivamente, pero siempre puede cambiarlos utilizando las
gc.get_threshold gc.set_threshold
.
Algoritmo de búsqueda de bucle
Una descripción completa del algoritmo de búsqueda de bucle requerirá un artículo separado. En resumen, GC itera sobre cada objeto de las generaciones seleccionadas y elimina temporalmente todos los enlaces de un solo objeto (todos los enlaces a los que se refiere este objeto). Después de un pase completo, todos los objetos con un recuento de enlaces de menos de dos se consideran inaccesibles desde Python y se pueden eliminar.
Para una comprensión más profunda, recomiendo leer (la nota del traductor: material en inglés) la
descripción original del algoritmo de Neil Schemenauer y la función de
collect
de las
fuentes de CPython . También puede ser útil una descripción de
Quora y una
publicación sobre el recolector de basura .
Vale la pena señalar que el problema con los destructores descritos en la descripción original del algoritmo se ha solucionado desde Python 3.4 (más detalles en
PEP 442 ).
Consejos de optimización
Los bucles a menudo ocurren en tareas de la vida real; se pueden encontrar en problemas con gráficos, listas vinculadas o en estructuras de datos en las que necesita realizar un seguimiento de las relaciones entre los objetos. Si su programa tiene una carga alta y exige demoras, entonces, si es posible, es mejor evitar los bucles.
En lugares donde a sabiendas usa enlaces circulares, puede usar enlaces "débiles". Los enlaces débiles se implementan en el módulo
weakref y, a diferencia de los enlaces normales, no afectan el contador de enlaces de ninguna manera. Si el objeto con referencias débiles resulta ser eliminado, entonces
None
devuelve
None
.
En algunos casos, es útil deshabilitar la compilación automática mediante el módulo gc y llamarlo manualmente. Para hacer esto, simplemente llame a
gc.disable()
y luego llame a
gc.collect()
manualmente.
Cómo encontrar y depurar enlaces circulares
La depuración de bucles puede ser dolorosa, especialmente si su código usa muchos módulos de terceros.
El módulo gc proporciona funciones auxiliares que pueden ayudar con la depuración. Si los parámetros del GC se establecen en el indicador
DEBUG_SAVEALL
, todos los objetos inaccesibles se agregarán a la lista
gc.garbage
.
import gc gc.set_debug(gc.DEBUG_SAVEALL) print(gc.get_count()) lst = [] lst.append(lst) list_id = id(lst) del lst gc.collect() for item in gc.garbage: print(item) assert list_id == id(item)
Una vez que identifique el punto problemático, puede visualizarse usando
objgraph .

Conclusión
El proceso principal de recolección de basura se realiza mediante un algoritmo de conteo de enlaces, que es muy simple y no tiene configuraciones. Un algoritmo adicional se usa solo para buscar y eliminar objetos con referencias circulares.
No debe involucrarse en la optimización prematura del código para el recolector de basura; en la práctica, los problemas con la recolección de basura son bastante raros.
PD: Soy el autor de este artículo, puedes hacer cualquier pregunta.