目次

1. Introducción

Público objetivo

Este artículo está dirigido principalmente a principiantes y usuarios intermedios que utilizan Python a diario. Es especialmente útil para quienes desean verificar el uso de memoria de sus programas y optimizarlo.

Objetivo del artículo

Los objetivos de este artículo son los siguientes:

  1. Comprender el mecanismo de gestión de memoria de Python.
  2. Aprender métodos concretos para medir el uso de memoria.
  3. Dominar técnicas de optimización para reducir el uso de memoria.

Al comprender este contenido, podrá ayudar a mejorar el rendimiento de los programas Python.

2. Fundamentos de la gestión de memoria de Python

Cómo funciona la gestión de memoria

En Python, la gestión de memoria se lleva a cabo mediante dos mecanismos principales: el recuento de referencias y la recolección de basura.

Recuento de referencias

El recuento de referencias es un mecanismo que cuenta cuántas veces cada objeto es referenciado.
En Python, cuando se crea un objeto, su recuento de referencias se establece en 1. Cada vez que otra variable referencia ese objeto, el recuento aumenta; cuando la referencia se elimina, el recuento disminuye. Cuando el recuento llega a 0, el objeto se libera automáticamente de la memoria.

Ejemplo de código
import sys

a = [1, 2, 3]  ## Se crea un objeto lista
print(sys.getrefcount(a))  ## Recuento de referencias inicial (normalmente 2, incluyendo referencias internas)

b = a  ## Otra variable referencia el mismo objeto
print(sys.getrefcount(a))  ## El recuento de referencias aumenta

del b  ## La referencia se libera
print(sys.getrefcount(a))  ## El recuento de referencias disminuye

Recolección de basura

La recolección de basura (Garbage Collection, GC) es un mecanismo que recupera memoria que no puede ser liberada por el recuento de referencias (especialmente referencias circulares). En Python, el recolector de basura incorporado funciona periódicamente y elimina automáticamente los objetos innecesarios.

El recolector de basura está especializado en la detección y liberación de referencias circulares, y resulta útil en situaciones como las siguientes:

class Node:
    def __init__(self):
        self.next = None

## Ejemplo de referencia circular
a = Node()
b = Node()
a.next = b
b.next = a

## En este estado, el recuento de referencias no llega a cero y la memoria no se libera

Si desea manipular el recolector de basura de forma explícita, puede controlar su comportamiento usando el módulo gc.

import gc

## Ejecutar el recolector de basura de forma forzada
gc.collect()

Riesgo de fugas de memoria

La gestión de memoria de Python es muy potente, pero no es perfecta. En particular, existen riesgos de fugas de memoria en situaciones como las siguientes:

  1. Referencias circulares están presentes pero el recolector de basura está desactivado.
  2. En programas de larga duración, los objetos innecesarios pueden permanecer en la memoria.

Para prevenir estos problemas, es importante diseñar evitando referencias circulares y eliminar explícitamente los objetos innecesarios.

Resumen de esta sección

  • La gestión de memoria de Python se lleva a cabo mediante los mecanismos de recuento de referencias y recolección de basura.
  • La recolección de basura es útil especialmente para resolver referencias circulares, pero es importante prevenir el consumo innecesario de memoria mediante un diseño adecuado.
  • En la siguiente sección, se explicará cómo medir concretamente el uso de memoria.

3. Cómo comprobar el uso de memoria

Método básico

sys.getsizeof() para confirmar el tamaño del objeto

Al usar la función getsizeof() incluida en el módulo sys de la biblioteca estándar de Python, se puede obtener el tamaño de memoria de cualquier objeto en bytes.

Ejemplo de código
import sys

## Verificar el tamaño de memoria de cada objeto
x = 42
y = [1, 2, 3, 4, 5]
z = {"a": 1, "b": 2}

print(f"tamaño de x: {sys.getsizeof(x)} bytes")
print(f"tamaño de y: {sys.getsizeof(y)} bytes")
print(f"tamaño de z: {sys.getsizeof(z)} bytes")
Puntos a considerar
  • sys.getsizeof() permite obtener solo el tamaño del propio objeto; no incluye el tamaño de otros objetos referenciados (por ejemplo, los elementos dentro de una lista).
  • Para medir con precisión el uso de memoria de objetos de gran escala, se requieren herramientas adicionales.

Uso de herramientas de perfilado

Medición de memoria por función con memory_profiler

memory_profiler es una biblioteca externa que permite medir detalladamente el uso de memoria de un programa Python por función. Facilita identificar fácilmente cuánto memoria consume cada parte del código.

Configuración

Primero, instala memory_profiler:

pip install memory-profiler
Modo de uso

@profile decorador permite medir el consumo de memoria por función.

from memory_profiler import profile

@profile
def example_function():
    a = [i for i in range(10000)]
    b = {i: i**2 for i in range(1000)}
    return a, b

if __name__ == "__main__":
    example_function()

En tiempo de ejecución, usa el siguiente comando:

python -m memory_profiler your_script.py
Ejemplo de salida
Line ##    Mem usage    Increment   Line Contents
------------------------------------------------
     3     13.1 MiB     13.1 MiB   @profile
     4     16.5 MiB      3.4 MiB   a = [i for i in range(10000)]
     5     17.2 MiB      0.7 MiB   b = {i: i**2 for i in range(1000)}

Monitorear el uso total de memoria del proceso con psutil

psutil es una biblioteca potente que permite monitorear el uso total de memoria de un proceso. Es útil cuando se desea conocer el consumo total de memoria de un script o aplicación específica.

Configuración

Instálalo con el siguiente comando:

pip install psutil
Modo de uso
import psutil

process = psutil.Process()
print(f"Uso total de memoria del proceso: {process.memory_info().rss / 1024**2:.2f} MB")
Características principales
  • Posibilidad de obtener el uso de memoria del proceso actual en bytes.
  • Permite obtener pistas de optimización mientras se monitorea el rendimiento del programa.

Seguimiento detallado de memoria

Rastrear asignaciones de memoria con tracemalloc

Al usar tracemalloc de la biblioteca estándar de Python, se puede rastrear el origen de las asignaciones de memoria y analizar qué partes consumen más memoria.

Modo de uso
import tracemalloc

## Iniciar seguimiento de memoria
tracemalloc.start()

## Proceso que consume memoria
a = [i for i in range(100000)]

## Mostrar uso de memoria
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")

print("[Uso de memoria]")
for stat in top_stats[:5]:
    print(stat)
Usos principales
  • Identificar los puntos problemáticos de asignación de memoria.
  • Comparar múltiples procesos para encontrar oportunidades de optimización.

Resumen de esta sección

  • Para comprender el uso de memoria en Python, existen muchas opciones, desde herramientas básicas como sys.getsizeof() hasta herramientas de perfilado como memory_profiler y psutil.
  • Si el consumo de memoria del programa es crítico, elige la herramienta adecuada y gestiona eficientemente.
  • En la siguiente sección, se explicarán métodos concretos para optimizar el uso de memoria.

4. Cómo optimizar el uso de memoria

Selección de estructuras de datos eficientes

Reemplazo de listas por generadores

Las comprensiones de listas son convenientes, pero consumen mucha memoria al manejar grandes volúmenes de datos. En su lugar, usar generadores permite generar los datos de forma secuencial, reduciendo significativamente el uso de memoria.

Ejemplo de código
## Uso de lista
list_data = [i**2 for i in range(1000000)]
print(f"Tamaño de memoria de la lista: {sys.getsizeof(list_data) / 1024**2:.2f} MB")

## Uso de generador
gen_data = (i**2 for i in range(1000000))
print(f"Tamaño de memoria del generador: {sys.getsizeof(gen_data) / 1024**2:.2f} MB")

Al usar generadores, se puede reducir significativamente el uso de memoria.

Como alternativa a los diccionarios, collections.defaultdict

Los diccionarios de Python son útiles, pero consumen mucha memoria al manejar datos a gran escala. Usar collections.defaultdict permite establecer valores predeterminados de manera eficiente y simplificar el procesamiento.

Ejemplo de código
from collections import defaultdict

## Diccionario normal
data = {}
data["key"] = data.get("key", 0) + 1

## Usar defaultdict
default_data = defaultdict(int)
default_data["key"] += 1

Gestión de objetos innecesarios

Eliminación explícita mediante la sentencia del

En Python, es posible eliminar manualmente los objetos innecesarios, lo que reduce la carga del recolector de basura.

Ejemplo de código
## Eliminar variables innecesarias
a = [1, 2, 3]
del a

Después de la eliminación, la variable a se libera de la memoria.

Uso del recolector de basura

Con el módulo gc, se puede ejecutar manualmente el recolector de basura, lo que permite resolver fugas de memoria causadas por referencias circulares.

Ejemplo de código
import gc

## Ejecución del recolector de basura
gc.collect()

Optimización mediante bibliotecas externas

Uso de NumPy y Pandas

NumPy y Pandas están diseñados para gestionar la memoria de manera eficiente. Especialmente al manejar grandes volúmenes de datos numéricos, usar estas bibliotecas permite reducir significativamente el uso de memoria.

Ejemplo de uso de NumPy
import numpy as np

## Lista Python
data_list = [i for i in range(1000000)]
print(f"Tamaño de memoria de la lista: {sys.getsizeof(data_list) / 1024**2:.2f} MB")

## Array NumPy
data_array = np.arange(1000000)
print(f"Tamaño de memoria del array NumPy: {data_array.nbytes / 1024**2:.2f} MB")

Se observa que los arrays de NumPy son más eficientes en memoria que las listas.

Prevención de fugas de memoria

Para prevenir fugas de memoria, es importante tener en cuenta los siguientes puntos.

  1. Evitar referencias circulares Diseñar los objetos para que no se referencien mutuamente.
  2. Control del alcance Tener en cuenta el alcance de funciones y clases para no dejar objetos innecesarios.

Resumen de esta sección

  • Para optimizar el uso de memoria, es importante elegir estructuras de datos eficientes y eliminar adecuadamente los objetos innecesarios.
  • Al aprovechar bibliotecas externas como NumPy o Pandas, se puede lograr una gestión de memoria aún más eficiente.
  • En la siguiente sección, se explicará la solución de problemas útil para la resolución de casos reales.
RUNTEQ(ランテック)|超実戦型エンジニア育成スクール

5. Solución de problemas

Cómo abordar un aumento repentino del uso de memoria

Ajustar el recolector de basura

Si el recolector de basura no funciona correctamente, la memoria no necesaria no se libera y el uso puede aumentar rápidamente. Para resolver este problema, use el módulo gc para ajustar el recolector de basura.

Ejemplo de código
import gc

## Recolector de basura: verificar el estado de funcionamiento
print(gc.get_threshold())

## Recolector de basura: ejecutar manualmente
gc.collect()

## Recolector de basura: cambiar la configuración (ejemplo: ajustar el umbral)
gc.set_threshold(700, 10, 10)

Revisar el ciclo de vida de los objetos

Algunos objetos pueden permanecer en memoria incluso después de que ya no sean necesarios. En ese caso, considere revisar el ciclo de vida del objeto y eliminarlo en el momento adecuado.

Fugas de memoria por referencias circulares

Resumen del problema

Las referencias circulares ocurren cuando dos o más objetos se referencian mutuamente. En ese caso, el recuento de referencias no llega a cero y el recolector de basura no puede liberar la memoria.

Soluciones
  • Utilizar referencias débiles (módulo weakref) para evitar referencias circulares.
  • Ejecutar manualmente el recolector de basura para resolver las referencias circulares.
Ejemplo de código
import weakref

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

a = Node("A")
b = Node("B")

## Evitar referencias circulares usando referencias débiles
a.next = weakref.ref(b)
b.next = weakref.ref(a)

Cuando la herramienta de perfilado de memoria no funciona

Error de memory_profiler

Al usar memory_profiler, puede ocurrir que el decorador @profile no funcione. Este problema se debe a que el script no se está ejecutando correctamente.

Soluciones
  1. Ejecute el script con la opción -m memory_profiler:
python -m memory_profiler your_script.py
  1. Asegúrese de que la función a la que se aplica el decorador está especificada correctamente.

Error de psutil

Si psutil no puede obtener información de memoria, puede haber un problema con la versión de la biblioteca o el entorno.

Soluciones
  1. Verifique la versión de psutil e instale la última versión:
pip install --upgrade psutil
  1. Asegúrese de que está obteniendo la información del proceso de la manera correcta:
import psutil
   process = psutil.Process()
   print(process.memory_info())

Cómo abordar errores por falta de memoria

Resumen del problema

Al manejar datos a gran escala, el programa puede generar un error de falta de memoria (MemoryError).

Soluciones
  • Reducir el tamaño de los datos Eliminando datos innecesarios y usando estructuras de datos eficientes.
## Uso de generador
   large_data = (x for x in range(10**8))
  • Realizar procesamiento por lotes Dividiendo los datos en fragmentos pequeños para reducir la cantidad de memoria consumida en cada operación.
for chunk in range(0, len(data), chunk_size):
       process_data(data[chunk:chunk + chunk_size])
  • Utilizar almacenamiento externo Guardando los datos en disco en lugar de en memoria para procesarlos (p.ej., SQLite, HDF5).

Resumen de esta sección

  • Utilice el recolector de basura y la gestión del ciclo de vida para controlar adecuadamente el uso de memoria.
  • Si aparecen referencias circulares o errores de herramientas, pueden resolverse con referencias débiles y configuraciones correctas.
  • Los errores por falta de memoria pueden evitarse revisando las estructuras de datos, procesando por lotes o utilizando almacenamiento externo.

6. Ejemplo práctico: Medición del uso de memoria en scripts de Python

Aquí se muestra un ejemplo concreto de cómo medir el uso de memoria dentro de un script de Python utilizando las herramientas y técnicas explicadas hasta ahora. A través de este ejemplo práctico, aprenderás a analizar y optimizar el uso de memoria.

Escenario de muestra: Comparación del uso de memoria entre listas y diccionarios

Ejemplo de código

El siguiente script mide el uso de memoria de listas y diccionarios usando sys.getsizeof() y memory_profiler.

import sys
from memory_profiler import profile

@profile
def compare_memory_usage():
    ## Creación de la lista
    list_data = [i for i in range(100000)]
    print(f"Uso de memoria de la lista: {sys.getsizeof(list_data) / 1024**2:.2f} MB")

    ## Creación del diccionario
    dict_data = {i: i for i in range(100000)}
    print(f"Uso de memoria del diccionario: {sys.getsizeof(dict_data) / 1024**2:.2f} MB")

    return list_data, dict_data

if __name__ == "__main__":
    compare_memory_usage()

Pasos de ejecución

  1. memory_profiler si no tienes instalado, ejecuta lo siguiente:
pip install memory-profiler
  1. Ejecuta el script con memory_profiler:
python -m memory_profiler script_name.py

Ejemplo de salida

Line ##    Mem usage    Increment   Line Contents
------------------------------------------------
     5     13.2 MiB     13.2 MiB   @profile
     6     17.6 MiB      4.4 MiB   list_data = [i for i in range(100000)]
     9     22.2 MiB      4.6 MiB   dict_data = {i: i for i in range(100000)}

Uso de memoria de la lista: 0.76 MB
Uso de memoria del diccionario: 3.05 MB

De este ejemplo se observa que los diccionarios consumen más memoria que las listas. Esto proporciona criterios para elegir la estructura de datos adecuada según los requisitos de la aplicación.

Escenario de muestra: Monitoreo del uso total de memoria del proceso

Ejemplo de código

El siguiente script usa psutil para monitorear en tiempo real el uso total de memoria del proceso.

import psutil
import time

def monitor_memory_usage():
    process = psutil.Process()
    print(f"Uso inicial de memoria: {process.memory_info().rss / 1024**2:.2f} MB")

    ## Simulación del consumo de memoria
    data = [i for i in range(10000000)]
    print(f"Uso de memoria durante el procesamiento: {process.memory_info().rss / 1024**2:.2f} MB")

    del data
    time.sleep(2)  ## Esperar la ejecución del recolector de basura
    print(f"Uso de memoria después de eliminar los datos: {process.memory_info().rss / 1024**2:.2f} MB")

if __name__ == "__main__":
    monitor_memory_usage()

Pasos de ejecución

  1. psutil si no tienes instalado, ejecuta lo siguiente:
pip install psutil
  1. Ejecuta el script:
python script_name.py

Ejemplo de salida

Uso de memoria inicial: 12.30 MB
Uso de memoria durante el procesamiento: 382.75 MB
Uso de memoria después de eliminar datos: 13.00 MB

Con este resultado puedes observar el comportamiento cuando grandes cantidades de datos consumen memoria y cómo la memoria se libera al eliminar objetos innecesarios.

Puntos clave de esta sección

  • Para medir el uso de memoria, es importante combinar adecuadamente herramientas (sys.getsizeof(), memory_profiler, psutil, etc.).
  • Visualizar el uso de memoria de estructuras de datos y del proceso completo permite identificar cuellos de botella y posibilita un diseño de programa más eficiente.

7. Resumen y siguientes pasos

Puntos clave del artículo

  1. Fundamentos de la gestión de memoria en Python
  • Python utiliza el “conteo de referencias” y la “recolección de basura” para gestionar automáticamente la memoria.
  • Para evitar problemas de referencias circulares, es necesario un diseño adecuado.
  1. Métodos para verificar el uso de memoria
  • sys.getsizeof() puedes verificar el tamaño de memoria a nivel de objeto.
  • memory_profiler y @psutil, puedes medir detalladamente el consumo de memoria de funciones o de todo el proceso.
  1. Métodos para optimizar el uso de memoria
  • Al usar generadores y estructuras de datos eficientes (p. ej., arrays de NumPy), puedes reducir el consumo de memoria al procesar grandes volúmenes de datos.
  • Eliminar objetos innecesarios y aprovechar el recolector de basura ayuda a prevenir fugas de memoria.
  1. Aplicación en ejemplos prácticos
  • A través de código real, aprendimos los pasos para medir la memoria y los métodos de optimización.
  • Practicaste la diferencia de uso de memoria entre listas y diccionarios, y ejemplos de monitoreo de memoria de todo el proceso.

Próximos pasos

  1. Aplicar en tu propio proyecto
  • Incorpora los métodos y herramientas presentados en este artículo en tus proyectos cotidianos de Python.
  • Por ejemplo, prueba @memory_profiler en scripts que manejan grandes volúmenes de datos para identificar los puntos con mayor consumo de memoria.
  1. Aprender una gestión de memoria más avanzada
  1. Uso de herramientas y servicios externos
  • En proyectos a gran escala, usar las
RUNTEQ(ランテック)|超実戦型エンジニア育成スクール