Saltar al contenido principal
InicioTutorialesPython

Introducción al perfilado de memoria en Python

Aprende cómo el perfilado de memoria en Python puede optimizar el rendimiento con herramientas como memory_profiler, tracemalloc y objgraph.
Actualizado 30 jul 2024  · 14 min leer

Las aplicaciones Python, especialmente las que trabajan con grandes conjuntos de datos o cálculos complejos, pueden consumir demasiada memoria, provocando ralentizaciones, bloqueos o errores por falta de memoria. El perfilado de memoria es una técnica que te permite analizar el uso de memoria de tu código Python, identificar fugas de memoria y cuellos de botella, y optimizar el consumo de memoria para mejorar el rendimiento y la estabilidad.

En este tutorial, exploraremos el perfilado de memoria, las herramientas disponibles y ejemplos prácticos de cómo utilizar estas herramientas para perfilar tu código Python.

¿Qué es el perfilado de memoria?

El perfilado de memoria es la técnica de monitorizar y evaluar la utilización de memoria de un programa mientras se ejecuta. Este método ayuda a los desarrolladores a encontrar fugas de memoria, optimizar la utilización de la memoria y comprender los patrones de consumo de memoria de sus programas. El perfilado de la memoria es crucial para evitar que las aplicaciones utilicen más memoria de la necesaria y provoquen un rendimiento lento o fallos.

Ventajas del perfilado de memoria

La creación de perfiles de memoria ofrece varias ventajas clave que son esenciales para mantener aplicaciones eficientes y robustas:

  • Identifica las fugas de memoria: Encuentra secciones de código que no estén liberando memoria correctamente.
  • Optimiza el uso de la memoria: Localiza las funciones u objetos que utilizan demasiada memoria.
  • Mejora el rendimiento: Mejora la velocidad y la capacidad de respuesta de las aplicaciones.
  • Evita las colisiones: Evita errores por falta de memoria.

Herramientas populares de perfilado de memoria

Exploremos varias herramientas populares de perfilado de memoria, detallando sus características, instalación y uso. En concreto, veremos herramientas como memory_profiler para analizar la memoria línea por línea, tracemalloc para tomar y comparar instantáneas de memoria, y objgraph para visualizar las relaciones entre objetos.

memory_profiler

memory_profiler es una potente herramienta de Python que proporciona una visión detallada del uso de memoria de tu código. Funciona proporcionando un desglose granular, línea por línea, del uso de memoria, lo que resulta útil para localizar las líneas exactas de código responsables de un elevado consumo de memoria. Al comprender el uso de la memoria con este nivel de detalle, los desarrolladores pueden tomar decisiones informadas sobre dónde centrar sus esfuerzos de optimización.

Instalación

Para instalar memory_profiler y los paquetes adicionales que sean necesarios, sigue estos pasos:

  • Abre el terminal o el símbolo del sistema.
  • En Windows, utilizaremos el Símbolo del sistema o PowerShell.
  • Podemos utilizar el terminal en MacOS o Linux.

El siguiente comando instala el paquete memory_profiler:

pip install memory_profiler

Si quieres visualizar el uso de memoria con gráficos, puedes instalar matplotlib ejecutando:

pip install matplotlib

Utilización

Para utilizar memory_profiler, sigue estos pasos:

  • Decoramos las funciones que queremos perfilar.

  • Abre tu script Python en un editor de texto o IDE.

  • Importa el decorador de perfiles de memory_profiler y aplícalo a las funciones que quieras perfilar.

Aquí tienes un script de ejemplo: 

from memory_profiler import profile
@profile

def allocate_memory():
    # Allocate a list with a range of numbers
    a = [i for i in range(10000)]
    # Allocate another list with squares of numbers
    b = [i ** 2 for i in range(10000)]
    return a, b

if __name__ == "__main__":
    allocate_memory()

Explicación del código

Aquí importo el decorador de perfiles del paquete memory_profiler. Este decorador nos ayudará a controlar el uso de memoria de la función decorada.

from memory_profiler import profile

Aplico el decorador @profile a la función allocate_memory(). Esto significa que cuando se ejecute allocate_memory(), memory_profiler hará un seguimiento de su uso de memoria y proporcionará un informe detallado.

@profile

Dentro de la función allocate_memory(), creo dos listas. La primera lista contiene números del 0 al 9999; la segunda lista contiene los cuadrados de los números del 0 al 9999.

a = [i for i in range(10000)]  # List of numbers from 0 to 9999

b = [i**2 for i in range(10000)]  # List of squares of numbers from 0 to 9999

El siguiente bloque garantiza que sólo se llame a la función allocate_memory() cuando se ejecute directamente el script, no cuando se importe como módulo. Cuando ejecutes este script, memory_profiler mostrará el uso de memoria de la función allocate_memory(), lo que te permitirá ver cuánta memoria se utiliza durante la creación de las dos listas, a y b.

if __name__ == "__main__":

   allocate_memory()

En tu terminal o símbolo del sistema, navega hasta el directorio donde se encuentra tu script. Ejecuta tu script con la opción -m memory_profiler.

-m memory_profiler your_script.py

Uso de memoria de cada línea de código. Imagen del autor.

En el gráfico anterior, podemos ver el uso de memoria de cada línea de código y los incrementos de cada línea de código. El gráfico siguiente muestra con más detalle cómo utiliza la memoria cada línea y cómo aumenta con cada iteración.

Uso de la memoria a lo largo del tiempo. Imagen del autor.

tracemalloc

tracemalloc es un potente módulo integrado en Python que realiza un seguimiento de las asignaciones y desasignaciones de memoria. Comprender cómo utiliza la memoria tu aplicación es crucial para optimizar el rendimiento e identificar las fugas de memoria. Tracemalloc proporciona una interfaz fácil de usar para capturar y analizar instantáneas del uso de la memoria, lo que la convierte en una herramienta inestimable para cualquier desarrollador de Python.

Examinemos cómo tracemalloc toma instantáneas del uso de memoria, compara esas instantáneas para identificar fugas de memoria y, a continuación, analiza los principales consumidores de memoria

Tomar instantáneas del uso de la memoria

tracemalloc te permite tomar instantáneas del uso de memoria en puntos concretos de tu programa. Una instantánea captura el estado de las asignaciones de memoria, proporcionando una visión detallada de dónde se utiliza la memoria.

A continuación te explicamos cómo puedes hacer una instantánea con tracemalloc:

import tracemalloc

# Start tracing memory allocations
tracemalloc.start()

# Code block for which memory usage is to be tracked
def allocate_memory():
    a = [i for i in range(10000)]  # List of numbers from 0 to 9999
    b = [i**2 for i in range(10000)]  # List of squares of numbers from 0 to 9999
    return a, b

# Keep the allocated lists in scope
allocated_lists = allocate_memory()

# Take a snapshot
snapshot = tracemalloc.take_snapshot()

# Stop tracing memory allocations
tracemalloc.stop()

# Analyze the snapshot to see memory usage
top_stats = snapshot.statistics('lineno')

# Write the memory usage data to a text file
with open('memory_usage.txt', 'w') as f:
    f.write("[ Top 10 memory consumers ]\n")
    for stat in top_stats[:10]:
        f.write(f"{stat}\n")
    # Detailed traceback for the top memory consumer
    f.write("\n[ Detailed traceback for the top memory consumer ]\n")
    for stat in top_stats[:1]:
        f.write('\n'.join(stat.traceback.format()) + '\n')

print("Memory usage details saved to 'memory_usage.txt'")

He rastreado y examinado el uso de memoria en mi aplicación Python utilizando el módulo tracemalloc. Tomando una instantánea de las asignaciones de memoria y observando los principales consumidores de memoria, determiné qué secciones de mi código utilizaban más memoria. A continuación, pongo este análisis en un archivo de texto para su posterior lectura y referencia.

Comparar instantáneas para identificar fugas de memoria

Una de las funciones más potentes de tracemalloc es la posibilidad de comparar instantáneas tomadas en distintos puntos de tu programa para identificar fugas de memoria. Analizando las diferencias entre las instantáneas, puedes localizar dónde se está asignando memoria adicional y no se está liberando.

A continuación te explicamos cómo comparar instantáneas:

import tracemalloc

# Start tracing memory allocations
tracemalloc.start()

# Function to allocate memory
def allocate_memory():
    a = [i for i in range(10000)]  # List of numbers from 0 to 9999
    b = [i**2 for i in range(10000)]  # List of squares of numbers from 0 to 9999
    return a, b

# Initial memory usage snapshot
snapshot1 = tracemalloc.take_snapshot()

# Perform memory allocations
allocated_lists = allocate_memory()

# Memory usage snapshot after allocations
snapshot2 = tracemalloc.take_snapshot()

# Further memory allocations (simulate more work to find potential leaks)
allocated_lists += allocate_memory()

# Memory usage snapshot after additional allocations
snapshot3 = tracemalloc.take_snapshot()

# Stop tracing memory allocations
tracemalloc.stop()

# Compare the snapshots
stats1_vs_2 = snapshot2.compare_to(snapshot1, 'lineno')
stats2_vs_3 = snapshot3.compare_to(snapshot2, 'lineno')

# Save the comparison results to a text file
with open('memory_leak_analysis.txt', 'w') as f:
    f.write("[ Memory usage increase from snapshot 1 to snapshot 2 ]\n")
    for stat in stats1_vs_2[:10]:
        f.write(f"{stat}\n")

    f.write("\n[ Memory usage increase from snapshot 2 to snapshot 3 ]\n")
    for stat in stats2_vs_3[:10]:
        f.write(f"{stat}\n")

    # Detailed traceback for the top memory consumers
    f.write("\n[ Detailed traceback for the top memory consumers ]\n")
    for stat in stats2_vs_3[:1]:
        f.write('\n'.join(stat.traceback.format()) + '\n')

print("Memory leak analysis saved to 'memory_leak_analysis.txt'")

Pasando de la instantánea 1 a la instantánea 2, vemos que el establecimiento de las listas a y b fue la causa principal de un pico en el uso de memoria. Si pasamos de la instantánea 2 de sa la instantánea 3, también vemos que más asignaciones de las listas a y b aumentaron la utilización de memoria.

Comparando estas instantáneas, descubrimos que la causa principal del aumento del uso de memoria es la asignación recurrente de grandes listas. Estas asignaciones pueden indicar una fuga de memoria si no son necesarias en aplicaciones del mundo real. Un rastreo exhaustivo puede identificar las líneas de código precisas que causan estas asignaciones.

Análisis de los principales consumidores de memoria

tracemalloc también proporciona herramientas para analizar los principales consumidores de memoria de tu aplicación. Esto puede ayudarte a centrarte en optimizar las partes de tu código que utilizan más memoria.

He aquí cómo analizar a los principales consumidores de memoria:

import tracemalloc

# Start tracing memory allocations
tracemalloc.start()

# Code block for which memory usage is to be tracked
def allocate_memory():
    a = [i for i in range(10000)]
    b = [i**2 for i in range(10000)]

allocate_memory()

# Take a snapshot
snapshot = tracemalloc.take_snapshot()

# Analyze top memory consumers
top_stats = snapshot.statistics('lineno')
print("[ Top 10 memory consumers ]")
for stat in top_stats[:10]:
    print(stat)

# Stop tracing memory allocations
tracemalloc.stop()

En este ejemplo, después de tomar una instantánea, utilizo instantánea. Estadísticas ('lineno') para obtener estadísticas sobre el uso de memoria agrupadas por número de línea. Este resultado te ayuda a identificar rápidamente qué partes de tu código consumen más memoria, lo que te permite centrarte en esas áreas para optimizarlas.

objgraph

Objgraph es una biblioteca de Python diseñada para ayudar a los desarrolladores a visualizar y analizar las referencias a objetos en su código. Proporciona herramientas para crear gráficos visuales que muestran cómo los objetos de la memoria se referencian entre sí. Esta visualización es beneficiosa para comprender la estructura y las relaciones dentro de tu programa, identificar fugas de memoria y optimizar el uso de la memoria.

Instalación

En primer lugar, vamos a instalar la biblioteca objgraph. Puedes instalarlo con pip:

pip install objgraph

Características principales de objgraph

objgraph ofrece varias funciones críticas para el perfilado y la visualización de la memoria:

  1. Generar Gráficos Interactivos de Referencias a Objetos: Conobjgraph, puedes crear representaciones visuales de objetos y sus referencias. Esto te ayuda a comprender cómo están interconectados los objetos, haciendo más accesible la depuración y optimización de tu código.
  2. Detectar referencias circulares: Las referencias circulares se producen cuando dos o más objetos se referencian entre sí, creando un bucle que el recolector de basura no puede resolver. Objgraph ayuda a identificar estas referencias circulares, que pueden provocar fugas de memoria si no se abordan.
  3. Analizar tipos y tamaños de objetos: Objgraph te permite examinar los tipos y tamaños de los objetos en memoria. Esta información es crucial para optimizar el uso de la memoria y mejorar el rendimiento de tu aplicación.

Utilizar objgraph para visualizar las relaciones entre objetos

Vamos a sumergirnos en algunos ejemplos para ver cómo se puede utilizar objgraph en la práctica.

Ejemplo 1: Generar un gráfico de referencia de objetos

Supongamos que tenemos una clase sencilla y creamos algunas instancias:

class Node:
    def __init__(self, value):
        self.value = value
        self.children = []

# Create some nodes
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)

# Establish relationships
node1.children.append(node2)
node2.children.append(node3)

import Objgraph

# Generate a graph of object references
objgraph.show_refs([node1], filename='object_refs.png')

En este ejemplo, definimos una clase Node y creamos instancias de nodos con relaciones. La función objgraph.show_refs() genera un gráfico visual de las referencias a objetos a partir de node1 y lo guarda como object_refs.png. Este gráfico nos ayuda a comprender cómo están interconectados los nodos.

Ejemplo 2: Detectar referencias circulares

Para detectar referencias circulares, podemos utilizar la función objgraph.find_backref_chain():

# Create a circular reference
node 3.children.append(node1)

# Detect circular references
chain = objgraph.find_backref_chain(node1, objgraph.is_proper_module)
objgraph.show_chain(chain, filename='circular_refs.png')

Aquí creamos una referencia circular añadiendo el nodo1 a los hijos del nodo3. La función objgraph.find_backref_chain() detecta esta referencia circular, y objgraph.show_chain la visualiza, guardando el resultado como circular_refs.png.

Ejemplo 3: Analizar tipos y tamaños de objetos

Para analizar los tipos y tamaños de los objetos, podemos utilizar objgraph.show_most_common_types y objgraph.by_type:

# Show the most common object types
objgraph.show_most_common_types()

# Get the details of a specific object type
objs = objgraph.by_type('Node')
objgraph.show_refs(objs[:3], refcounts=True, filename='node_details.png')

En este ejemplo, objgraph.mostrar_tipos_más_comunes muestra los tipos de objetos más comunes que hay actualmente en memoria. La función objgraph.by_type() recupera todas las instancias de la clase Nodo, y visualizamos sus referencias, incluido el recuento de referencias, generando otro gráfico guardado como detalles_nodo.png.

Cómo utilizar las herramientas de creación de perfiles de memoria

Veamos ahora varias técnicas para utilizar eficazmente las herramientas de perfilado de memoria. Nos centraremos en el perfilado de funciones específicas, el seguimiento del uso general de memoria y la visualización de gráficos de objetos.

Perfilar funciones específicas

Dirígete a funciones específicas con memory_profiler o tracemalloc.

Ejemplo con memory_profiler

memory_profiler ofrece una forma sencilla de controlar el consumo de memoria línea por línea dentro de tus funciones, ayudándote a identificar las áreas en las que se puede reducir el uso de memoria.

from memory_profiler import profile

@profile

def allocate_memory():
    a = [i for i in range(10000)]
    b = [i ** 2 for i in range(10000)]
    return a, b

if __name__ == "__main__":
    allocate_memory()

Ejemplo con tracemalloc

import tracemalloc

tracemalloc.start()

def allocate_memory():
    a = [i for i in range(10000)]
    b = [i ** 2 for i in range(10000)]
    return a, b

allocate_memory()

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

for stat in top_stats[:10]:
    print(stat)

Seguimiento del uso global de la memoria

Para controlar el uso total de memoria de nuestro programa a lo largo del tiempo, podemos utilizar el módulo incorporado tracemalloc. He aquí cómo podemos seguir el uso general de la memoria e interpretar los resultados para identificar tendencias.

Pasos a seguir tracemalloc

Comenzamosomenzamos inicializando el rastreo de memoria al inicio de nuestro script.

import tracemalloc

tracemalloc.start()

A continuación, executa nuestro código ejecutando las secciones de nuestro código que queremos controlar.

def run_heavy_computation():
   a = [i for i in range(10000)]
   b = [i**2 for i in range(10000)]
   return a, b

run_heavy_computation()

Enhacemos instantáneas del uso de la memoria. Esto nos permite capturar el estado de las asignaciones de memoria en diferentes puntos.

snapshot1 = tracemalloc.take_snapshot()

run_heavy_computation()

snapshot2 = tracemalloc.take_snapshot()

Por último, comparamos instantáneas para comprender los patrones de uso de la memoria y detectar fugas.

stats = snapshot2.compare_to(snapshot1, 'lineno')

for stat in stats[:10]:
   print(stat)

Ejemplo

import tracemalloc

# Start tracing memory allocations
tracemalloc.start()

# Function to run heavy computation
def run_heavy_computation():
    a = [i for i in range(10000)]
    b = [i**2 for i in range(10000)]
    return a, b

# Initial memory usage snapshot
snapshot1 = tracemalloc.take_snapshot()

# Perform memory allocations
run_heavy_computation()

# Memory usage snapshot after allocations
snapshot2 = tracemalloc.take_snapshot()

# Compare the snapshots
stats = snapshot2.compare_to(snapshot1, 'lineno')

# Print the top memory-consuming lines of code
print("[ Top 10 memory consumers ]")
for stat in stats[:10]:
    print(stat)

# Stop tracing memory allocations
tracemalloc.stop()

 Consejos para interpretar los resultados

Aquí tienes algunos consejos que te ayudarán a dar sentido a los datos de tus perfiles:

  • Busca cambios significativos: Céntrate en líneas con incrementos de memoria sustanciales.
  • Identifica los puntos calientes: Determina qué líneas o funciones consumen sistemáticamente más memoria.
  • Seguimiento en el tiempo: Toma varias instantáneas en distintas fases del programa para identificar tendencias y señalar cuándo y dónde se producen picos de uso de memoria.
  • Comprueba si hay fugas de memoria: Compara las instantáneas tomadas antes y después de operaciones significativas para detectar fugas. Los aumentos persistentes del uso de memoria suelen indicar fugas.

Visualizar gráficos de objetos

Utilizando la biblioteca objgraph, podemos crear visualizaciones de las relaciones entre objetos. Esto nos ayuda a analizar el uso de la memoria, detectar fugas y comprender el tiempo de vida de los objetos.

He aquí el proceso paso a paso para visualizar gráficos de objetos. Primero, generaremos un gráfico de referencias a objetos; después, detectaremos las referencias circulares; por último, analizaremos los gráficos. 

 Generar un gráfico de referencia de objetos

import objgraph

class Node:
   def __init__(self, value):
       self.value = value
       self.children = []

# Create some nodes and establish relationships
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.children.append(node2)
node2.children.append(node3)

# Generate a graph of object references
objgraph.show_refs([node1], filename='object_refs.png')

Detectar referencias circulares

# Create a circular reference
node3.children.append(node1)

# Detect circular references
chain = objgraph.find_backref_chain(node1, objgraph.is_proper_module)

objgraph.show_chain(chain, filename='circular_refs.png')

Analizar tipos y tamaños de objetos

# Show the most common object types
objgraph.show_most_common_types()

# Get details of a specific object type
nodes = objgraph.by_type('Node')

objgraph.show_refs(nodes[:3], refcounts=True, filename='node_details.png')

 Ejemplo

import objgraph

class Node:
    def __init__(self, value):
        self.value = value
        self.children = []

# Create some nodes and establish relationships
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.children.append(node2)
node2.children.append(node3)

# Generate a graph of object references
objgraph.show_refs([node1], filename='object_refs.png')

# Create a circular reference
node3.children.append(node1)

# Detect circular references
chain = objgraph.find_backref_chain(node1, objgraph.is_proper_module)
objgraph.show_chain(chain, filename='circular_refs.png')

# Show the most common object types
objgraph.show_most_common_types()

# Get details of a specific object type
nodes = objgraph.by_type('Node')
objgraph.show_refs(nodes[:3], refcounts=True, filename='node_details.png')

Analiza los gráficos

Como paso final, analizamos los gráficos. Consideramos los siguientes pasos en orden:

  • Identifica las conexiones: Observa cómo se conectan los objetos para comprender las dependencias y los tiempos de vida.
  • Detecta fugas: Encuentra objetos que deberían haberse recogido de la basura, pero que siguen en memoria, lo que indica posibles fugas.
  • Referencias circulares puntuales: Identifica las referencias circulares que pueden impedir la recogida de basuras y provocar la sobrecarga de memoria.
  • Optimiza el uso de la memoria: Utiliza la información de los gráficos para refactorizar nuestro código y mejorar la eficiencia de la memoria.

Consejos prácticos y buenas prácticas

He aquí algunos consejos clave y buenas prácticas que te ayudarán a sacar el máximo partido a tus esfuerzos de elaboración de perfiles:

  1. Perfil Temprano: Perfila pronto y a menudo en el proceso de desarrollo.
  2. Utiliza datos realistas: Utiliza datos y cargas de trabajo realistas para elaborar perfiles precisos.
  3. Combina herramientas: Combina varias herramientas para una comprensión global.
  4. Cuidado con los falsos positivos: Ten cuidado con los posibles falsos positivos en los perfiles de memoria.
  5. Optimiza las partes que consumen mucha memoria: Céntrate en optimizar las partes de tu código que consumen más memoria.

Conclusión

En este tutorial, hemos explorado herramientas y técnicas esenciales para comprender y optimizar el uso de la memoria en tus aplicaciones Python. Estas técnicas incluían memory_profiler, que ofrece una forma directa de controlar el consumo de memoria línea por línea dentro de tu función, y objgraph, que proporciona potentes capacidades de visualización, permitiéndote analizar las relaciones entre objetos, detectar referencias circulares y comprender la estructura de tu uso de memoria.

Integrar herramientas de creación de perfiles de memoria en tu flujo de trabajo de desarrollo te permite obtener información valiosa sobre cómo gestiona la memoria tu código, te ayuda a tomar decisiones de optimización informadas y a mejorar el rendimiento y la fiabilidad generales de tus aplicaciones. Tanto si trabajas en pequeños scripts como en sistemas a gran escala, el perfilado de memoria es una práctica esencial para cualquier desarrollador de Python que pretenda escribir código eficiente y robusto.

Para profundizar tus conocimientos y habilidades en el perfilado de memoria y la escritura de código eficiente, considera la posibilidad de realizar el Curso de Escritura de Código Eficiente en Python. Además, nuestro curso de Programación en Python ofrece una visión completa de las mejores prácticas y técnicas avanzadas para ayudarte a convertirte en un desarrollador competente.


Temas

Aprende Python con DataCamp

Certificación disponible

Course

Introducción a las funciones en Python

3 hr
416.6K
Aprende el arte de escribir tus propias funciones en Python, así como conceptos clave como el alcance y la gestión de errores.
See DetailsRight Arrow
Start Course
Ver másRight Arrow