3 étapes pour une refonte asynchrone Python

Python est l'un des nombreux languges qui prennent en charge une manière d'écrire des programmes asynchrones - des programmes qui basculent librement entre plusieurs tâches, toutes s'exécutant à la fois, de sorte qu'aucune tâche ne retarde la progression des autres.

Cependant, il est fort probable que vous ayez principalement écrit des programmes Python synchrones - des programmes qui ne font qu'une chose à la fois, attendant que chaque tâche se termine avant d'en démarrer une autre. Passer à l'asynchrone peut être déroutant, car cela nécessite non seulement d'apprendre une nouvelle syntaxe, mais aussi de nouvelles façons de penser son code. 

Dans cet article, nous allons explorer comment un programme synchrone existant peut être transformé en programme asynchrone. Cela implique plus que la simple décoration de fonctions avec une syntaxe asynchrone; cela nécessite également de réfléchir différemment à la façon dont notre programme fonctionne et de décider si async est même une bonne métaphore de ce qu'il fait. 

[Aussi sur: Découvrez les trucs et astuces Python à partir des vidéos Smart Python de Serdar Yegulalp]

Quand utiliser async en Python

Un programme Python est le mieux adapté pour asynchrone lorsqu'il présente les caractéristiques suivantes:

  • Il essaie de faire quelque chose qui est principalement lié par les E / S ou en attendant la fin d'un processus externe, comme une lecture réseau de longue durée.
  • Il essaie de faire un ou plusieurs de ces types de tâches à la fois, tout en gérant peut-être également les interactions des utilisateurs.
  • Les tâches en question ne sont pas lourdes en calcul.

Un programme Python qui utilise le threading est généralement un bon candidat pour utiliser async. Les threads en Python sont coopératifs; ils se cèdent au besoin. Les tâches asynchrones en Python fonctionnent de la même manière. De plus, async offre certains avantages par rapport aux threads:

  • La syntaxe async/ awaitfacilite l'identification des parties asynchrones de votre programme. En revanche, il est souvent difficile de dire en un coup d'œil quelles parties d'une application s'exécutent dans un thread. 
  • Étant donné que les tâches asynchrones partagent le même thread, toutes les données auxquelles elles accèdent sont gérées automatiquement par le GIL (mécanisme natif de Python pour synchroniser l'accès aux objets). Les threads nécessitent souvent des mécanismes complexes de synchronisation. 
  • Les tâches asynchrones sont plus faciles à gérer et à annuler que les threads.

L'utilisation d'async n'est pas recommandée si votre programme Python présente les caractéristiques suivantes:

  • Les tâches ont un coût de calcul élevé - par exemple, elles effectuent de lourds calculs. Le travail de calcul lourd est mieux géré avec multiprocessing, ce qui vous permet de consacrer un thread matériel entier à chaque tâche.
  • Les tâches ne bénéficient pas de l'entrelacement. Si chaque tâche dépend de la dernière, il est inutile de les exécuter de manière asynchrone. Cela dit, si le programme implique des  ensembles de tâches série, vous pouvez exécuter chaque ensemble de manière asynchrone.

Étape 1: Identifiez les parties synchrones et asynchrones de votre programme

Le code asynchrone Python doit être lancé et géré par les parties synchrones de votre application Python. À cette fin, votre première tâche lors de la conversion d'un programme en asynchrone consiste à tracer une ligne entre les parties sync et async de votre code.

Dans notre précédent article sur l'async, nous avons utilisé une application Web Scraper comme exemple simple. Les parties asynchrones du code sont les routines qui ouvrent les connexions réseau et lisent à partir du site - tout ce que vous voulez entrelacer. Mais la partie du programme qui lance tout cela n'est pas asynchrone; il lance les tâches asynchrones, puis les ferme gracieusement au fur et à mesure qu'elles se terminent.

Il est également important de séparer toute opération potentiellement  bloquante de l'async et de la conserver dans la partie synchronisation de votre application. La lecture des entrées utilisateur depuis la console, par exemple, bloque tout, y compris la boucle d'événement asynchrone. Par conséquent, vous souhaitez gérer les entrées utilisateur avant de lancer les tâches asynchrones ou après les avoir terminées. (Il est possible de gérer les entrées utilisateur de manière asynchrone via le multitraitement ou le threading, mais c'est un exercice avancé que nous n'entrerons pas ici.)

Quelques exemples d'opérations de blocage:

  • Entrée de la console (comme nous venons de le décrire).
  • Tâches impliquant une utilisation intensive du processeur.
  • Utilisation time.sleeppour forcer une pause. Notez que vous pouvez dormir dans une fonction asynchrone en utilisant asyncio.sleepcomme substitut de time.sleep.

Étape 2: Convertissez les fonctions de synchronisation appropriées en fonctions asynchrones

Une fois que vous savez quelles parties de votre programme s'exécuteront de manière asynchrone, vous pouvez les partitionner en fonctions (si vous ne l'avez pas déjà fait) et les transformer en fonctions asynchrones avec le asyncmot - clé. Vous devrez ensuite ajouter du code à la partie synchrone de votre application pour exécuter le code asynchrone et en recueillir les résultats si nécessaire.

Remarque: Vous voudrez vérifier la chaîne d'appels de chaque fonction que vous avez rendue asynchrone et vous assurer qu'elle n'invoque pas une opération potentiellement longue ou bloquante. Les fonctions async peuvent appeler directement des fonctions de synchronisation, et si cette fonction de synchronisation bloque, la fonction async qui l'appelle également.

Examinons un exemple simplifié du fonctionnement d'une conversion de synchronisation vers asynchrone. Voici notre programme «avant»:

def a_function (): # une action compatible asynchrone qui prend un certain temps def another_function (): # une fonction de synchronisation, mais pas une fonction de blocage def do_stuff (): a_function () another_function () def main (): for _ in range (3): do_stuff () main () 

Si nous voulons que trois instances de do_stuffs'exécutent en tant que tâches asynchrones, nous devons transformer do_stuff (et potentiellement tout ce qu'elle touche) en code asynchrone. Voici un premier passage à la conversion:

import asyncio async def a_function (): # une action compatible async qui prend un certain temps def another_function (): # une fonction de synchronisation, mais pas une fonction de blocage async def do_stuff (): wait a_function () another_function () async def main ( ): tasks = [] pour _ dans la plage (3): tasks.append (asyncio.create_task (do_stuff ())) attend asyncio.gather (tâches) asyncio.run (main ()) 

Notez les modifications que nous avons apportées  main. main Utilise maintenant asynciopour lancer chaque instance de en do_stufftant que tâche simultanée, puis attend les résultats ( asyncio.gather). Nous avons également converti a_functionen une fonction asynchrone, car nous voulons que toutes les instances de a_functions'exécutent côte à côte, et avec toutes les autres fonctions nécessitant un comportement asynchrone.

Si nous voulions aller plus loin, nous pourrions également convertir another_functionen asynchrone:

async def another_function (): # une fonction de synchronisation, mais pas une fonction de blocage async def do_stuff (): wait a_function () wait another_function () 

Cependant, rendre  another_function asynchrone serait exagéré, car (comme nous l'avons noté) cela ne fait rien qui bloque la progression de notre programme. De plus, si des parties synchrones de notre programme sont appelées  another_function, nous devrons également les convertir en asynchronisation, ce qui pourrait rendre notre programme plus compliqué qu'il ne le faut.

Étape 3: Testez soigneusement votre programme asynchrone Python

Tout programme converti de manière asynchrone doit être testé avant sa mise en production pour s'assurer qu'il fonctionne comme prévu.

Si votre programme est de taille modeste - disons, quelques dizaines de lignes environ - et n'a pas besoin d'une suite de tests complète, alors il ne devrait pas être difficile de vérifier qu'il fonctionne comme prévu. Cela dit, si vous convertissez le programme en asynchrone dans le cadre d'un projet plus vaste, où une suite de tests est un appareil standard, il est logique d'écrire des tests unitaires pour les composants async et sync.

Les deux principaux frameworks de test en Python proposent désormais une sorte de support asynchrone. Le propre unittest framework de Python  comprend des objets de cas de test pour les fonctions asynchrones et pytestpropose  pytest-asyncioles mêmes fins.

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
  • Comment utiliser timeit pour profiler le code Python
  • Comment utiliser cProfile pour profiler le code Python
  • Comment convertir Python en JavaScript (et inversement)