Perfilando el código de Python utilizando timeit y cProfile

Optimizing Python code with timeit and cProfile

 

Como desarrollador de software, es probable que hayas escuchado la frase “La optimización prematura es la raíz de todos los males” más de una vez en tu carrera. Si bien la optimización puede no ser muy útil (o absolutamente necesaria) para proyectos pequeños, el perfilado a menudo es útil.

Después de terminar de codificar un módulo, es una buena práctica perfilar tu código para medir cuánto tiempo tarda en ejecutarse cada una de las secciones. Esto puede ayudar a identificar malos olores de código y guiar las optimizaciones para mejorar la calidad del código. ¡Así que siempre perfila tu código antes de optimizarlo!

Para dar los primeros pasos, esta guía te ayudará a comenzar con el perfilado en Python, utilizando los módulos integrados timeit y cProfile. Aprenderás a utilizar tanto la interfaz de línea de comandos como las funciones equivalentes dentro de los scripts de Python.

 

Cómo perfilar código Python utilizando timeit

 

El módulo timeit es parte de la biblioteca estándar de Python y ofrece algunas funciones de conveniencia que se pueden utilizar para medir el tiempo de ejecución de fragmentos de código cortos.

Tomemos un ejemplo simple de invertir una lista de Python. Mediremos los tiempos de ejecución de obtener una copia invertida de la lista utilizando:

  • la función reversed(), y
  • el slicing de la lista. 
>>> nums=[6,9,2,3,7]
>>> list(reversed(nums))
[7, 3, 2, 9, 6]
>>> nums[::-1]
[7, 3, 2, 9, 6]

 

 

Ejecución de timeit en la línea de comandos

 

Puedes ejecutar timeit en la línea de comandos utilizando la siguiente sintaxis:

$ python -m timeit -s 'código-de-configuración' -n 'número' -r 'repetir' 'stmt'

 

Debes proporcionar el enunciado stmt cuyo tiempo de ejecución se medirá. 

Puedes especificar el código de configuración setup cuando sea necesario, utilizando la opción corta -s o la opción larga –setup. El código de configuración se ejecutará solo una vez.

El número de veces que se ejecutará el enunciado: opción corta -n o opción larga –number es opcional. Y el número de veces que se repetirá este ciclo: opción corta -r o opción larga –repeat también es opcional.

Veamos esto en acción para nuestro ejemplo:

En este caso, la creación de la lista es el código de configuración y invertir la lista es el enunciado que se medirá:

$ python -m timeit -s 'nums=[6,9,2,3,7]' 'list(reversed(nums))'
500000 loops, mejor de 5: 695 nsec por loop

 

Cuando no especificas valores para repeat, se utiliza el valor predeterminado de 5. Y cuando no especificas number, el código se ejecuta tantas veces como sea necesario para alcanzar un tiempo total de al menos 0.2 segundos.

Este ejemplo establece explícitamente el número de veces que se ejecutará el enunciado:

$ python -m timeit -s 'nums=[6,9,2,3,7]' -n 100Bu000 'list(reversed(nums))'
100000 loops, mejor de 5: 540 nsec por loop

 

El valor predeterminado de repeat es 5, pero podemos establecerlo en cualquier valor adecuado:

$ python3 -m timeit -s 'nums=[6,9,2,3,7]' -r 3 'list(reversed(nums))'
500000 loops, mejor de 3: 663 nsec por loop

 

También vamos a medir el enfoque de slicing de la lista:

$ python3 -m timeit -s 'nums=[6,9,2,3,7]' 'nums[::-1]'
1000000 loops, mejor de 5: 142 nsec por loop

 

El enfoque de división de listas parece ser más rápido (todos los ejemplos están en Python 3.10 en Ubuntu 22.04).

 

Ejecutando timeit en un script de Python

 

Aquí está el equivalente de ejecutar timeit dentro del script de Python:

import timeit

setup = 'nums=[9,2,3,7,6]'
number = 100000
stmt1 = 'list(reversed(nums))'
stmt2 = 'nums[::-1]'

t1 =  timeit.timeit(setup=setup,stmt=stmt1,number=number)
t2 = timeit.timeit(setup=setup,stmt=stmt2,number=number)

print(f"Utilizando la función reversed(): {t1}")
print(f"Utilizando la división de listas: {t2}")

 

El callable timeit() devuelve el tiempo de ejecución de stmt para number veces. Observa que podemos mencionar explícitamente el número de veces a ejecutar, o hacer que number tome el valor predeterminado de 1000000.

Output >>
Utilizando la función reversed(): 0.08982690000000002
Utilizando la división de listas: 0.015550800000000004

 

Esto ejecuta la declaración, sin repetir la función del temporizador, durante el número especificado de veces y devuelve el tiempo de ejecución. También es bastante común utilizar time.repeat() y tomar el tiempo mínimo como se muestra a continuación:

import timeit

setup = 'nums=[9,2,3,7,6]'
number = 100000
stmt1 = 'list(reversed(nums))'
stmt2 = 'nums[::-1]'

t1 =  min(timeit.repeat(setup=setup,stmt=stmt1,number=number))
t2 = min(timeit.repeat(setup=setup,stmt=stmt2,number=number))

print(f"Utilizando la función reversed(): {t1}")
print(f"Utilizando la división de listas: {t2}")

 

Esto repetirá el proceso de ejecutar el código el número de veces especificado número de veces y devuelve el tiempo de ejecución mínimo. Aquí tenemos 5 repeticiones de 100000 veces cada una.

Output >>
Utilizando la función reversed(): 0.055375300000000016
Utilizando la división de listas: 0.015101400000000043

 

Cómo perfilar un script de Python utilizando cProfile

 

Hemos visto cómo timeit se puede utilizar para medir los tiempos de ejecución de fragmentos de código cortos. Sin embargo, en la práctica, es más útil perfilar un script de Python completo. 

Esto nos dará los tiempos de ejecución de todas las funciones y llamadas a métodos, incluyendo funciones y métodos integrados. Por lo tanto, podemos tener una mejor idea de las llamadas a funciones más costosas e identificar oportunidades de optimización. Por ejemplo: puede haber una llamada a una API que es demasiado lenta. O una función puede tener un bucle que puede ser reemplazado por una expresión de comprensión más pythonica. 

Aprendamos cómo perfilar scripts de Python utilizando el módulo cProfile (también parte de la biblioteca estándar de Python). 

Considera el siguiente script de Python:

# main.py
import time

def func(num):
    for i in range(num):
        print(i)

def another_func(num):
    time.sleep(num)
    print(f"Dormido durante {num} segundos")

def useful_func(nums, target):
    if target in nums:
        return nums.index(target)

if __name__ == "__main__":
    func(1000)
    another_func(20)
    useful_func([2, 8, 12, 4], 12)

 

Aquí tenemos tres funciones:

  • func() que recorre un rango de números y los imprime.
  • another func() que contiene una llamada a la función sleep().
  • useful_func() que devuelve el índice de un número objetivo en la lista (si el objetivo está presente en la lista).

Las funciones mencionadas anteriormente se llamarán cada vez que ejecutes el script main.py.

 

Ejecutando cProfile en la línea de comandos

 

Ejecuta cProfile en la línea de comandos usando:

python3 -m nombre-archivo.py

 

Aquí hemos nombrado el archivo main.py:

python3 -m main.py

 

Al ejecutar esto, deberías obtener la siguiente salida:

  Salida >>
  0
  ...
  999
  Durmió durante 20 segundos

 

Y el siguiente perfil:

   

Aquí, ncalls se refiere al número de llamadas a la función y percall se refiere al tiempo por cada llamada a la función. Si el valor de ncalls es mayor a uno, entonces percall es el tiempo promedio en todas las llamadas.

El tiempo de ejecución del script está dominado por another_func que utiliza la llamada a la función integrada sleep (duerme durante 20 segundos). También vemos que las llamadas a la función print son bastante costosas. 

 

Usando cProfile en el script de Python

 

Aunque ejecutar cProfile en la línea de comandos funciona bien, también puedes agregar la funcionalidad de perfilado al script de Python. Puedes usar cProfile junto con el módulo pstats para el perfilado y el acceso a las estadísticas.

Como una buena práctica para manejar mejor la configuración y la liberación de recursos, usa la declaración with y crea un objeto de perfil que se utiliza como un gestor de contexto:

# main.py
import pstats
import time
import cProfile

def func(num):
    for i in range(num):
        print(i)

def another_func(num):
    time.sleep(num)
    print(f"Durmio durante {num} segundos")

def useful_func(nums, target):
    if target in nums:
        return nums.index(target)


if __name__ == "__main__":
    with cProfile.Profile() as profile:
        func(1000)
        another_func(20)
        useful_func([2, 8, 12, 4], 12)
    profile_result = pstats.Stats(profile)
    profile_result.print_stats()

 

Echemos un vistazo más de cerca al perfil de salida generado:

   

Cuando estás perfilando un script grande, será útil ordenar los resultados por tiempo de ejecución. Para hacerlo, puedes llamar a sort_stats en el objeto de perfil y ordenar basándote en el tiempo de ejecución: 

...
if __name__ == "__main__":
    with cProfile.Profile() as profile:
        func(1000)
        another_func(20)
        useful_func([2, 8, 12, 4], 12)
    profile_result = pstats.Stats(profile)
    profile_result.sort_stats(pstats.SortKey.TIME)
    profile_result.print_stats()

 

Cuando ahora ejecutes el script, deberías poder ver los resultados ordenados por tiempo:

 

 

Conclusión

 

Espero que esta guía te ayude a comenzar con el perfilado en Python. Recuerda siempre que las optimizaciones nunca deben ser a expensas de la legibilidad. Si estás interesado en aprender sobre otros perfiles, incluyendo paquetes de Python de terceros, echa un vistazo a este artículo sobre perfiles de Python.     Bala Priya C es una desarrolladora y escritora técnica de India. Le gusta trabajar en la intersección de las matemáticas, la programación, la ciencia de datos y la creación de contenido. Sus áreas de interés y experiencia incluyen DevOps, ciencia de datos y procesamiento de lenguaje natural. Le gusta leer, escribir, programar y tomar café. Actualmente, está trabajando en aprender y compartir su conocimiento con la comunidad de desarrolladores mediante la creación de tutoriales, guías prácticas, artículos de opinión y más.  

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Noticias de Inteligencia Artificial

Acelere el ciclo de vida del desarrollo del chatbot de Amazon Lex con Test Workbench.

Amazon Lex se complace en anunciar Test Workbench, una nueva solución de prueba de bots que proporciona herramientas ...

Inteligencia Artificial

Visión a través del sonido para los ciegos

Los científicos han creado tecnología de toque acústico que utiliza el sonido para ayudar a las personas ciegas a ver.

Inteligencia Artificial

Hoja de ruta de Aprendizaje Automático Recomendaciones de la Comunidad 2023

En el último artículo, Parte 1 de este mapa de ruta, discutimos brevemente las herramientas iniciales y las direccion...

Inteligencia Artificial

Perplejidad revela dos nuevos modelos de LLM en línea 'pplx-7b-online' y 'pplx-70b-online

Perplexity, una innovadora startup de IA, ha introducido una solución para transformar los sistemas de recuperación d...

Inteligencia Artificial

Investigadores de Microsoft proponen PIT (Transformación Permutación Invariante) un compilador de aprendizaje profundo para la escasez dinámica.

Recientemente, el aprendizaje profundo se ha caracterizado por un aumento en la investigación orientada a optimizar m...