Mejorando scripts de Python con Cython

0 acciones
0
0
0
0

Introducción

Python es quizás uno de los lenguajes de programación más populares hoy en día, pero sin duda no es el más eficiente. Especialmente en el mundo del aprendizaje automático, los usuarios sacrifican eficiencia por la facilidad de uso que ofrece Python.

Pero eso no significa que no puedas acelerar las cosas de otras maneras. Cython es una forma sencilla de reducir el tiempo de cálculo de los scripts de Python sin sacrificar la funcionalidad que se consigue fácilmente con Python.

Este tutorial te enseñará a usar Cython para acelerar scripts de Python. Abordaremos una tarea sencilla pero computacionalmente costosa: crear un bucle `for` que recorra una lista de mil millones de números en Python y los sume. Dado que el tiempo es crucial al ejecutar código en dispositivos con recursos limitados, exploraremos este problema considerando cómo implementar código Python en Cython en una Raspberry Pi (RPi). Cython marca una diferencia significativa en la velocidad de cálculo; es como comparar un proceso lento con uno rápido.

Requisitos previos para optimizar scripts de Python con Cython

  • Conocimientos básicos de Python: Familiaridad con la sintaxis, las funciones, los tipos de datos y los módulos de Python.
  • Comprender conceptos básicos de C/C++: Familiaridad con conceptos básicos de C o C++ como punteros, tipos de datos y estructuras de control.
  • Entorno de desarrollo de Python: Instale Python (preferiblemente Python 3.x) con un administrador de paquetes como pip.
  • Instalar Cython: Instale Cython con el comando pip install cython.
  • Conocimientos básicos de terminal/línea de comandos: Capacidad para navegar y ejecutar comandos en una terminal o línea de comandos.

Estos requisitos previos te ayudarán a prepararte para comenzar a optimizar código Python usando Cython.

Python y CPython

Mucha gente desconoce que lenguajes como Python se implementan en otros lenguajes. Por ejemplo, la implementación de Python en C se conoce como CPython. Cabe destacar que es diferente de Cython. Para obtener más información sobre las distintas implementaciones de Python, puedes leer este artículo.

La implementación predeterminada y más popular de Python es C-Python. Su uso tiene una ventaja importante: C es un lenguaje compilado, y su código se convierte en código máquina que la unidad central de procesamiento (CPU) ejecuta directamente. Ahora bien, cabe preguntarse: si C es un lenguaje compilado, ¿significa eso que Python también lo es?

La implementación de Python en C (CPython) 100% no se compila ni se interpreta. De hecho, tanto la compilación como la interpretación ocurren durante la ejecución de un script de Python. Para aclarar esto, veamos los pasos que implica la ejecución de un script de Python:

  1. Compilar código fuente usando CPython para generar bytecode.
  2. Interpretación de bytecode por el intérprete CPython
  3. Ejecutar la salida del intérprete de CPython en la máquina virtual de CPython

El proceso de compilación ocurre cuando CPython compila el código fuente (archivo .py) y genera el bytecode de CPython (archivo .pyc). Este bytecode es interpretado por el intérprete de CPython y el resultado se ejecuta en la máquina virtual de CPython. Como se muestra en los pasos anteriores, la ejecución de un script de Python implica tanto compilación como interpretación.

El compilador de CPython genera bytecode una sola vez, pero el intérprete se llama cada vez que se ejecuta el código. Interpretar bytecode suele llevar mucho tiempo. Si usar un intérprete ralentiza la ejecución, ¿por qué usarlo? La razón principal es que el intérprete permite usar Python en distintos sistemas operativos. Dado que el bytecode se ejecuta independientemente de la máquina en la máquina virtual de CPython que se ejecuta en la CPU, se puede ejecutar en distintas máquinas sin necesidad de modificaciones.

Si no se utiliza un intérprete, el compilador de CPython generará código máquina que se ejecuta directamente en la CPU. Debido a que las distintas plataformas tienen instrucciones diferentes, el código no se puede ejecutar en todas ellas.

En resumen, usar un compilador acelera el proceso, pero el intérprete hace que el código sea multiplataforma. Por lo tanto, una de las razones por las que Python es más lento que C es por el uso del intérprete. Recuerda que el compilador se ejecuta solo una vez, mientras que el intérprete se ejecuta cada vez que se ejecuta el código.

Python es mucho más lento que C, pero muchos programadores aún lo prefieren porque es mucho más fácil de usar. Python oculta muchos detalles al programador, lo que puede evitar frustrantes tareas de depuración. Por ejemplo, como Python es un lenguaje de tipado dinámico, no es necesario especificar el tipo de cada variable en el código; Python lo infiere automáticamente. En cambio, en los lenguajes de tipado estático (como C, C++ o Java) es necesario especificar los tipos de las variables, como se muestra a continuación.

int x = 10
string s = "Hello"

Compárese con la siguiente implementación en Python:

El tipado dinámico facilita la codificación, pero exige más a la máquina para encontrar el tipo de dato correcto, lo que ralentiza el proceso de ejecución.

x = 10
s = "Hello"

En general, los lenguajes de alto nivel como Python son mucho más fáciles de entender para los desarrolladores. Sin embargo, al ejecutar el código, es necesario convertirlo a instrucciones de bajo nivel. Esta conversión requiere más tiempo, lo cual se sacrifica en aras de la facilidad de uso.

Si el tiempo apremia, conviene usar comandos de bajo nivel. Así, en lugar de escribir código en Python, que es la interfaz de usuario, se puede usar CPython, que internamente es Python implementado en C. Sin embargo, al hacerlo, la sensación será la de programar en C, no en Python.

CPython es mucho más complejo. En CPython, todo está implementado en C. No hay forma de evitar la complejidad de C al programar. Por eso, muchos desarrolladores usan Cython. Pero ¿en qué se diferencia Cython de CPython?

¿En qué se diferencia Cython?

Como se definió anteriormente, Cython es un lenguaje que ofrece lo mejor de ambos mundos: velocidad y facilidad de uso. Se puede seguir escribiendo código Python, pero para acelerar la ejecución, Cython permite reemplazar ciertas partes del código Python con código C. De esta forma, se combinan ambos lenguajes en un solo archivo. Cabe mencionar que todo lo que se puede implementar en Python es válido en Cython, aunque con algunas limitaciones.

Un archivo Python normal tiene la extensión .py, mientras que un archivo Cython tiene la extensión .pyx. El mismo código Python se puede escribir dentro de archivos .pyx, pero estos archivos también permiten usar código Cython. Ten en cuenta que colocar código Python en un archivo .pyx puede acelerar el proceso en comparación con ejecutar el código Python directamente, pero no será tan rápido como declarar los tipos de las variables. Por lo tanto, este tutorial no se centra únicamente en escribir código Python dentro de un archivo .pyx, sino en realizar modificaciones que optimicen su ejecución. Esto añade cierta complejidad a la programación, pero ahorra mucho tiempo. Si tienes experiencia con la programación en C, te resultará más fácil.

Citronización de código Python simple

Para convertir código Python a Cython, primero necesitas crear un archivo con la extensión . .píxide Crear en lugar de extensión .py. Dentro de este archivo, puede comenzar a escribir código Python regular (tenga en cuenta que existen algunas limitaciones en el código que acepta Cython, las cuales se explican en la documentación de Cython).

Antes de continuar, asegúrese de que Cython esté instalado. Puede hacerlo utilizando el siguiente comando.

pip install cython

Para generar el archivo .pyd/.so, primero necesitamos compilar el archivo Cython. El archivo .pyd/.so representa un módulo que se importará posteriormente. Se utiliza un archivo setup.py para compilar el archivo Cython. Cree este archivo e incluya el siguiente código. Usaremos la función distutils.core.setup() para llamar a la función Cython.Build.cythonize(), que compilará el archivo .pyx con Cython. Esta función acepta la ruta al archivo que se desea compilar con Cython. Aquí, se asume que el archivo setup.py se encuentra en la misma ubicación que el archivo test_cython.pyx.

import distutils.core
import Cython.Build
distutils.core.setup(
ext_modules = Cython.Build.cythonize("test_cython.pyx"))

Para generar el archivo Cython, introduzca el siguiente comando en la línea de comandos. Se espera que el directorio actual de la línea de comandos sea el mismo que el del archivo setup.py.

python setup.py build_ext --inplace

Una vez finalizado este comando, se crearán dos archivos junto al archivo .pyx. El primero tendrá la extensión .c y el segundo la extensión .pyd (o similar, según el sistema operativo). Para usar el archivo generado, basta con importar el módulo test_cython y se mostrará el mensaje “Hola Cython”, como se muestra a continuación.

Ya hemos compilado correctamente el código Python con Cyton. En la siguiente sección veremos cómo compilar un archivo .pyx que contiene un bucle.

Citonización de un bucle "for"“

Ahora optimicemos nuestra tarea anterior: un bucle `for` que recorre un millón de números y los suma. Comencemos analizando la eficiencia del bucle en sí. El módulo `time` nos permite estimar cuánto tiempo tarda en ejecutarse.

import time
t1 = time.time()
for k in range(1000000):
pass
t2 = time.time()
t = t2-t1
print("%.20f" % t)

En un archivo .pyx, el tiempo promedio de ejecución para 3 pruebas es de 0,0281 segundos. El código se ejecuta en una máquina con un procesador Core i7-6500U a 2,5 GHz y 16 GB de RAM DDR3.

Compárese esto con el tiempo de ejecución de un archivo Python típico, que tiene un promedio de 0,0411 segundos. Esto significa que Cython es solo 1,46 veces más rápido que Python para iteraciones, incluso sin tener que modificar el bucle `for` para que se ejecute a la velocidad de C.

Ahora agreguemos la suma. Para ello, utilizaremos la función range().

import time
t1 = time.time()
total = 0
for k in range(1000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.100f" % t)

Cabe destacar que ambos scripts devuelven el mismo valor: 499999500000. En Python, su ejecución tarda un promedio de 0,1183 segundos (según tres pruebas). En Cython, es 1,35 veces más rápido, con un promedio de 0,0875 segundos.

Ahora veamos otro ejemplo donde el bucle comienza en 0 y recorre mil millones de números.

import time
t1 = time.time()
total = 0
for k in range(1000000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.20f" % t)

El script de Cython finalizó en aproximadamente 85 segundos (1,4 minutos), mientras que el de Python tardó unos 115 segundos (1,9 minutos). En ambos casos, el proceso es muy lento. ¿Qué sentido tiene usar Cython si una tarea tan sencilla tarda más de un minuto? Cabe destacar que esto es un fallo nuestro, no de Cython.

Como se mencionó anteriormente, escribir código Python dentro de un script Cython (.pyx) representa una mejora, pero no supone una diferencia significativa en el tiempo de ejecución. Necesitamos realizar algunos cambios en el código Python dentro del script Cython. Lo primero que debemos hacer es declarar explícitamente el tipo de datos de las variables utilizadas.

Asignación de tipos de datos C a variables

Como en el código anterior, se utilizan 5 variables: total, k, t1, t2 y t. El tipo de dato de todas estas variables se infiere implícitamente, lo que aumenta el tiempo de cálculo. Para ahorrar tiempo, asignemos el tipo de dato directamente desde el lenguaje C.

La variable `total` es de tipo `unsigned long long int`. Es un entero porque la suma de todos los números es un entero, y es sin signo porque la suma siempre será positiva. ¿Por qué `long long`? Porque la suma de todos los números es muy grande, así que se usa `long long` para que la variable sea lo más grande posible.

El tipo de datos asignado a la variable k es int y a las tres variables restantes t1, t2 y t se les asigna el tipo de datos float.

import time
cdef unsigned long long int total
cdef int k
cdef float t1, t2, t
t1 = time.time()
for k in range(1000000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.100f" % t)

Observa que la precisión definida en la última instrucción print está establecida en 100 y todos estos números son cero (ver la siguiente imagen). Esto es lo que podemos esperar al usar Cython. Mientras que Python tarda más de 1,9 minutos, Cython es instantáneo. Ni siquiera diría que es 1000 o 100 000 veces más rápido que Python; probé con diferentes precisiones para el tiempo impreso y aun así no aparece ningún número.

Ten en cuenta que también puedes crear una variable entera para almacenar el valor que se pasa a la función `range()`. Esto mejorará aún más el rendimiento. El nuevo código se muestra a continuación, donde el valor se almacena en la variable entera `maxval`.

import time
cdef unsigned long long int maxval
cdef unsigned long long int total
cdef int k
cdef float t1, t2, t
maxval=1000000000
t1=time.time()
for k in range(maxval):
total = total + k
print "Total =", total
t2=time.time()
t = t2-t1
print("%.100f" % t)

Ahora que hemos visto cómo aumentar el rendimiento de los scripts de Python usando Cython, apliquemos esto a la Raspberry Pi (RPi).

Acceso a Raspberry Pi desde un ordenador personal

Si es la primera vez que usas tu Raspberry Pi, tendrás que conectar tanto tu PC como tu RPi a una red. Puedes hacerlo conectando ambos dispositivos a un switch con DHCP (Protocolo de Configuración Dinámica de Host) habilitado para asignar direcciones IP automáticamente. Una vez creada la red, podrás acceder a tu RPi mediante la dirección IPv4 que se le haya asignado. ¿Cómo saber qué dirección IPv4 tiene tu RPi? No te preocupes, puedes usar un escáner de IP. En este tutorial, usaré una aplicación gratuita llamada Advanced IP Scanner.

La interfaz de usuario de esta aplicación es la siguiente. Esta aplicación acepta un rango de direcciones IPv4 para buscar y devuelve información sobre los dispositivos activos.

Necesitas introducir el rango de direcciones IPv4 de tu red local. Si no conoces este rango, ejecuta el comando `ipconfig` en Windows (o `ifconfig` en Linux) para encontrar la dirección IPv4 de tu ordenador (como se muestra en la figura a continuación). En mi caso, la dirección IPv4 asignada al adaptador Wi-Fi de mi ordenador es 192.168.43.177 y la máscara de subred es 255.255.255.0. Esto significa que el rango de direcciones IPv4 en la red va de 192.168.43.1 a 192.168.43.255. Como se muestra en la figura, la dirección IPv4 192.168.43.1 está asignada a la puerta de enlace. Ten en cuenta que la última dirección IPv4 de este rango, 192.168.43.255, está reservada para mensajes de difusión. Entonces, el rango que debe buscar comienza en 192.168.43.2 y termina en 192.168.43.254.

Según el resultado del escaneo que se muestra en la siguiente figura, la dirección IPv4 asignada a la RPi es 192.168.43.63. Esta dirección IPv4 se puede utilizar para establecer una sesión Secure Shell (SSH).

Para establecer una sesión SSH, utilizaré un software gratuito llamado MobaXterm. La interfaz de usuario de este programa es la siguiente.

Para crear una sesión SSH, simplemente haga clic en el botón Sesión en la esquina superior izquierda. Aparecerá una nueva ventana como se muestra a continuación.

Desde esta ventana, haga clic en el botón SSH de la esquina superior izquierda para abrir la ventana que se muestra a continuación. Introduzca la dirección IPv4 y el nombre de usuario de la Raspberry Pi (que por defecto es «pi») y haga clic en Aceptar para iniciar la sesión.

Tras hacer clic en el botón Aceptar, aparecerá una nueva ventana donde deberá introducir la contraseña. La contraseña predeterminada es raspberrypi. Tras iniciar sesión, aparecerá la siguiente ventana. El panel izquierdo le permitirá navegar fácilmente por los directorios de su Raspberry Pi. También dispone de una línea de comandos para introducir comandos.

Usando Cython con Raspberry Pi

Crea un archivo nuevo y cambia su extensión a .pyx para escribir el código del ejemplo anterior. En la barra lateral izquierda encontrarás opciones para crear archivos y directorios. Puedes usar el icono de nuevo archivo para simplificar el proceso, como se muestra en la imagen a continuación. Creé un archivo llamado test_cython.pyx en el directorio raíz de la Raspberry Pi.

Haz doble clic en el archivo para abrirlo, pega el código y guárdalo. A continuación, debemos crear el archivo setup.py, que será exactamente como lo vimos anteriormente. Después, debemos ejecutar el siguiente comando para compilar el script de Cython.

python3 setup.py build_ext --inplace

Una vez que este comando se complete correctamente, encontrará los archivos de salida en el panel izquierdo, como se muestra en la siguiente figura. Tenga en cuenta que la extensión del módulo a importar ahora es .so, ya que ya no estamos usando Windows.

Ahora activemos Python e importemos el módulo, como se muestra a continuación. Se obtienen los mismos resultados que en un PC; el tiempo de ejecución es prácticamente nulo.

Resultado

Este tutorial analizó cómo usar Cython para reducir el tiempo de cálculo al ejecutar scripts de Python. Mostraremos un ejemplo del uso de un bucle. para Analizamos la suma de todos los elementos de una lista de mil millones de números en Python y comparamos el tiempo de ejecución con y sin la declaración de variables. Mientras que este proceso tarda casi dos minutos en Python puro, al usar Cython y declarar variables estáticas, se ejecuta prácticamente al instante.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

También te puede gustar