3 pasos para una revisión de Python async

Python es uno de los muchos idiomas que admiten alguna forma de escribir programas asincrónicos: programas que cambian libremente entre varias tareas, todas ejecutándose a la vez, de modo que ninguna tarea detiene el progreso de las demás.

Sin embargo, lo más probable es que haya escrito principalmente programas de Python sincrónicos, programas que hacen solo una cosa a la vez, esperando que cada tarea termine antes de comenzar otra. Pasar a async puede ser discordante, ya que requiere aprender no solo una nueva sintaxis, sino también nuevas formas de pensar sobre el código. 

En este artículo, exploraremos cómo un programa sincrónico existente se puede convertir en uno asincrónico. Esto implica más que simplemente decorar funciones con sintaxis asíncrona; también requiere pensar de manera diferente sobre cómo se ejecuta nuestro programa y decidir si async es incluso una buena metáfora de lo que hace. 

[También en: Aprenda trucos y consejos sobre Python de los videos de Smart Python de Serdar Yegulalp]

Cuando usar async en Python

Un programa de Python es más adecuado para async cuando tiene las siguientes características:

  • Está tratando de hacer algo que está mayormente limitado por E / S o esperando a que se complete algún proceso externo, como una lectura de red de larga duración.
  • Está tratando de hacer uno o más de esos tipos de tareas a la vez, mientras posiblemente también maneja las interacciones del usuario.
  • Las tareas en cuestión no son computacionalmente pesadas.

Un programa de Python que usa subprocesos suele ser un buen candidato para usar async. Los hilos en Python son cooperativos; se entregan unos a otros según sea necesario. Las tareas asincrónicas en Python funcionan de la misma manera. Además, async ofrece ciertas ventajas sobre los subprocesos:

  • La sintaxis async/ awaitfacilita la identificación de las partes asincrónicas de su programa. Por el contrario, a menudo es difícil saber de un vistazo qué partes de una aplicación se ejecutan en un hilo. 
  • Debido a que las tareas asíncronas comparten el mismo hilo, cualquier información a la que acceden es administrada automáticamente por GIL (el mecanismo nativo de Python para sincronizar el acceso a los objetos). Los subprocesos a menudo requieren mecanismos complejos de sincronización. 
  • Las tareas asincrónicas son más fáciles de administrar y cancelar que los hilos.

No se recomienda usar async si su programa Python tiene estas características:

  • Las tareas tienen un alto costo computacional, por ejemplo, están haciendo un gran procesamiento de números. El trabajo computacional pesado se maneja mejor multiprocessing, lo que le permite dedicar un hilo de hardware completo a cada tarea.
  • Las tareas no se benefician de estar intercaladas. Si cada tarea depende de la última, no tiene sentido hacer que se ejecuten de forma asincrónica. Dicho esto, si el programa incluye  conjuntos de tareas en serie, puede ejecutar cada conjunto de forma asincrónica.

Paso 1: identifique las partes sincrónicas y asincrónicas de su programa

El código asíncrono de Python debe ser iniciado y administrado por las partes síncronas de su aplicación Python. Con ese fin, su primera tarea al convertir un programa a async es trazar una línea entre las partes de sincronización y async de su código.

En nuestro artículo anterior sobre async, usamos una aplicación web scraper como un ejemplo simple. Las partes asincrónicas del código son las rutinas que abren las conexiones de red y leen desde el sitio, todo lo que desea intercalar. Pero la parte del programa que inicia todo eso no es asincrónica; lanza las tareas asincrónicas y luego las cierra elegantemente cuando terminan.

También es importante separar cualquier operación de bloqueo potencial  de la asincrónica y mantenerla en la parte de sincronización de su aplicación. Leer la entrada del usuario desde la consola, por ejemplo, bloquea todo, incluido el bucle de eventos asíncronos. Por lo tanto, desea controlar la entrada del usuario antes de iniciar las tareas asíncronas o después de finalizarlas. (Se es posible la entrada del usuario mango de forma asíncrona a través de multiprocesamiento o roscado, pero eso es un ejercicio avanzado no vamos a entrar en aquí.)

Algunos ejemplos de operaciones de bloqueo:

  • Entrada de consola (como acabamos de describir).
  • Tareas que implican un uso intensivo de la CPU.
  • Utilizando time.sleeppara forzar una pausa. Tenga en cuenta que puede dormir dentro de una función asincrónica utilizando asyncio.sleepcomo sustituto de time.sleep.

Paso 2: convierta las funciones de sincronización adecuadas en funciones asíncronas

Una vez que sepa qué partes de su programa se ejecutarán de forma asincrónica, puede dividirlas en funciones (si aún no lo ha hecho) y convertirlas en funciones asíncronas con la asyncpalabra clave. Luego, deberá agregar código a la parte síncrona de su aplicación para ejecutar el código asincrónico y recopilar resultados de él si es necesario.

Nota: querrá comprobar la cadena de llamadas de cada función que ha hecho asincrónica y asegurarse de que no invocan una operación de bloqueo o de ejecución potencialmente prolongada. Las funciones asíncronas pueden llamar directamente a las funciones de sincronización, y si esa función de sincronización se bloquea, también lo hace la función asíncrona que la llama.

Veamos un ejemplo simplificado de cómo podría funcionar una conversión de sincronización a asincrónica. Aquí está nuestro programa "antes":

def a_function (): # alguna acción compatible con asíncrono que toma un tiempo def otra_función (): # alguna función de sincronización, pero no una de bloqueo def do_stuff (): a_function () otra_función () def main (): para _ en rango (3): hacer_cosas () principal () 

Si queremos que tres instancias de se do_stuffejecuten como tareas asíncronas, necesitamos convertir do_stuff (y potencialmente todo lo que toca) en código asíncrono. Aquí hay un primer paso en la conversión:

import asyncio async def a_function (): # alguna acción compatible con async que toma un tiempo def otra_función (): # alguna función de sincronización, pero no una de bloqueo async def do_stuff (): espera a_function () otra_función () async def main ( ): tasks = [] for _ in range (3): tasks.append (asyncio.create_task (do_stuff ())) await asyncio.gather (tasks) asyncio.run (main ()) 

Tenga en cuenta los cambios que le hicimos  main. Ahora main usa asynciopara lanzar cada instancia de do_stuffcomo una tarea concurrente, luego espera los resultados ( asyncio.gather). También lo convertimos a_functionen una función asíncrona, ya que queremos que todas las instancias de se a_functionejecuten en paralelo y junto con cualquier otra función que necesite un comportamiento asíncrono.

Si quisiéramos ir un paso más allá, también podríamos convertir another_functiona async:

async def another_function (): # alguna función de sincronización, pero no bloqueando una async def do_stuff (): await a_function () await another_function () 

Sin embargo, hacer  another_function asincrónico sería excesivo, ya que (como hemos señalado) no hace nada que bloquee el progreso de nuestro programa. Además, si se llamaba alguna parte síncrona de nuestro programa  another_function, también tendríamos que convertirla a asíncrona, lo que podría hacer que nuestro programa fuera más complicado de lo necesario.

Paso 3: prueba tu programa asincrónico de Python a fondo

Cualquier programa de conversión asíncrona debe probarse antes de entrar en producción para garantizar que funcione como se espera.

Si su programa es de tamaño modesto, digamos, un par de docenas de líneas más o menos, y no necesita un conjunto de pruebas completo, entonces no debería ser difícil verificar que funcione según lo previsto. Dicho esto, si está convirtiendo el programa a asíncrono como parte de un proyecto más grande, donde un conjunto de pruebas es un accesorio estándar, tiene sentido escribir pruebas unitarias para componentes asíncronos y sincronizados por igual.

Los dos marcos de prueba principales en Python ahora cuentan con algún tipo de soporte asíncrono. El propio unittest marco de Python  incluye objetos de casos de prueba para funciones asíncronas y pytestofertas  pytest-asynciopara los mismos fines.

Finally, when writing tests for async components, you’ll need to handle their very asynchronousness as a condition of the tests. For instance, there is no guarantee that async jobs will complete in the order they were submitted. The first one might come in last, and some might never complete at all. Any tests you design for an async function must take these possibilities into account.

How to do more with Python

  • Get started with async in Python
  • How to use asyncio in Python
  • How to use PyInstaller to create Python executables
  • Cython tutorial: How to speed up Python
  • How to install Python the smart way
  • How to manage Python projects with Poetry
  • How to manage Python projects with Pipenv
  • Virtualenv and venv: Python virtual environments explained
  • Python virtualenv and venv do’s and don’ts
  • Python threading and subprocesses explained
  • How to use the Python debugger
  • Cómo usar timeit para perfilar el código Python
  • Cómo usar cProfile para perfilar el código Python
  • Cómo convertir Python a JavaScript (y viceversa)