Fichiers et Asynchrone dans Python avec iofiles et asyncio

May 13, 2021
Rédigé par
Sam Agnew
Twilion

Fichiers et Asynchrone dans Python avec iofiles et asyncio

Le code asynchrone est devenu un pilier du développement Python. Comme asyncio fait partie de la librairie standard et que de nombreux packages tiers fournissent des fonctionnalités compatibles avec lui, ce paradigme n'est pas prêt de disparaître.

Si vous écrivez du code asynchrone, il est important de vous assurer que toutes les parties de votre code fonctionnent ensemble afin que l'un des aspects du code ne ralentisse pas tout le reste. Les E/S de fichiers peuvent être un blocage courant sur ce front. Voyons comment utiliser la librairie aiofiles pour travailler de manière asynchrone avec des fichiers.

En commençant par les bases, il s'agit de tout le code dont vous avez besoin pour lire le contenu d'un fichier de manière asynchrone (dans une fonction async) :

async with aiofiles.open('filename', mode='r') as f:
    contents = await f.read()
print(contents)

Voyons cela plus en détail.

Qu'est-ce qu'un code non bloquant ?

Vous pouvez entendre des termes tels que « asynchrone », « non bloquant » ou « simultané » et être un peu perdu quant à leur signification. Selon ce tutoriel beaucoup plus détaillé, deux des propriétés principales sont les suivantes :

  • Les routines asynchrones peuvent mettre en pause tout en attendant leur résultat final pour permettre à d'autres routines de s'exécuter en même temps.
  • Le code asynchrone, via le mécanisme ci-dessus, facilite l'exécution simultanée. Pour simplifier, le code asynchrone donne l'aspect de la simultanéité.

Le code asynchrone est donc un code qui peut se mettre en pause en attendant un résultat, afin de permettre à d'autres codes de s'exécuter entre-temps. Il ne « bloque » pas l'exécution d'un autre code et nous pouvons donc l'appeler code « non bloquant ».

Il librairie asyncio fournit une variété d'outils pour les développeurs Python, et les fichiers d'aide fournissent des fonctionnalités encore plus spécifiques pour travailler avec des fichiers.

Configuration

Assurez-vous de configurer votre environnement Python avant de commencer. Suivez ce guide dans la section virtualenv si vous avez besoin d'aide. Le bon fonctionnement de l'ensemble, en particulier en ce qui concerne les environnements virtuels, est important pour isoler vos dépendances si plusieurs projets s'exécutent sur la même machine. Vous aurez besoin d'au moins Python 3.7 ou une version ultérieure pour exécuter le code de ce post.

Maintenant que votre environnement est configuré, vous devez installer des librairies tierces. Nous allons utiliser aiohttp. Installez-la avec la commande suivante après avoir activé votre environnement virtuel :

pip install aiofiles==0.6.0

Pour les exemples de la suite de ce post, nous utiliserons des fichiers JSON de données API Pokemon correspondant aux 150 Pokémon originaux. Vous pouvez télécharger un dossier contenant tous ces éléments ici. Ensuite, vous pouvez passer à l'écriture du code.

Lecture à partir d'un fichier grâce à aiofiles

Commençons par ouvrir un fichier correspondant à un Pokemon particulier, analyser son JSON dans un dictionnaire et afficher son nom :

import aiofiles
import asyncio
import json


async def main():
    async with aiofiles.open('articuno.json', mode='r') as f:
        contents = await f.read()
    pokemon = json.loads(contents)
    print(pokemon['name'])

asyncio.run(main())

Lors de l'exécution de ce code, le message « articuno » doit s'afficher sur le terminal. Vous pouvez également itérer le fichier de manière asynchrone, ligne par ligne (ce code imprime les 9 271 lignes de articuno.json) :

import aiofiles
import asyncio

async def main():
    async with aiofiles.open('articuno.json', mode='r') as f:
        async for line in f:
            print(line)

asyncio.run(main())

Écriture dans un fichier grâce à aiofiles

L'écriture dans un fichier est également similaire aux E/S de fichier Python standard. Supposons que nous voulons créer des fichiers contenant une liste de toutes les attaques que chaque Pokémon peut apprendre. Pour un exemple simple, voici ce que nous ferions pour le Pokémon Métamorph (Ditto en anglais), qui ne peut apprendre que l'attaque « Morphing » (Transform en anglais) :

import aiofiles
import asyncio

async def main():
    async with aiofiles.open('ditto_moves.txt', mode='w') as f:
        await f.write('transform')

asyncio.run(main())

Essayons avec un Pokémon qui a plusieurs attaques, comme Rhinoféros (Rhydon en anglais) :

import aiofiles
import asyncio
import json


async def main():
    # Read the contents of the json file.
    async with aiofiles.open('rhydon.json', mode='r') as f:
        contents = await f.read()

    # Load it into a dictionary and create a list of moves.
    pokemon = json.loads(contents)
    name = pokemon['name']
    moves = [move['move']['name'] for move in pokemon['moves']]

    # Open a new file to write the list of moves into.
    async with aiofiles.open(f'{name}_moves.txt', mode='w') as f:
        await f.write('\n'.join(moves))


asyncio.run(main())

Si vous ouvrez rhydon_moves.txt, vous devriez voir un fichier avec 112 lignes qui commence de cette manière.

Un fichier texte contenant la liste des attaques que Rhinoféros peut apprendre

Utilisation d'asyncio pour parcourir de nombreux fichiers de manière asynchrone

Nous allons maintenant compliquer un peu les choses et nous allons le faire pour les 150 Pokémon pour lesquels nous avons des fichiers JSON. Notre code devra lire chaque fichier, analyser le JSON et réécrire chaque attaque de Pokémon vers un nouveau fichier :

import aiofiles
import asyncio
import json
from pathlib import Path


directory = 'directory/your/files/are/in'


async def main():
    pathlist = Path(directory).glob('*.json')

    # Iterate through all json files in the directory.
    for path in pathlist:
        # Read the contents of the json file.
        async with aiofiles.open(f'{directory}/{path.name}', mode='r') as f:
            contents = await f.read()

        # Load it into a dictionary and create a list of moves.
        pokemon = json.loads(contents)
        name = pokemon['name']
        moves = [move['move']['name'] for move in pokemon['moves']]

        # Open a new file to write the list of moves into.
        async with aiofiles.open(f'{directory}/{name}_moves.txt', mode='w') as f:
            await f.write('\n'.join(moves))


asyncio.run(main())

Après avoir exécuté ce code, vous devriez voir le répertoire des fichiers Pokémon remplis de fichiers .txt à côté des fichiers .json, contenant les listes d'attaques correspondant à chaque Pokémon.

La sortie d'une commande ls, affichant les fichiers json et les fichiers txt côte à côte

Si vous devez effectuer certaines actions asynchrones et que vous souhaitez terminer par des données correspondant à ces tâches asynchrones, telles qu'une liste avec chaque attaque de Pokémon après avoir écrit les fichiers, vous pouvez utiliser asyncio.ensure_future et asyncio.gather.

Vous pouvez décomposer la partie de votre code qui gère chaque fichier en sa propre fonction async et ajouter des promesses pour ces appels de fonction à une liste de tâches. Voici un exemple de cette fonction et de votre nouvelle fonction main :

async def write_pokemon_moves(filename):
    # Read the contents of the json file.
    async with aiofiles.open(f'{directory}/{filename}', mode='r') as f:
        contents = await f.read()

    # Load it into a dictionary and create a list of moves.
    pokemon = json.loads(contents)
    name = pokemon['name']
    moves = [move['move']['name'] for move in pokemon['moves']]

    # Open a new file to write the list of moves into.
    async with aiofiles.open(f'{directory}/{name}_moves.txt', mode='w') as f:
        await f.write('\n'.join(moves))
    return { 'name': name, 'moves': moves }


async def main():
    pathlist = Path(directory).glob('*.json')

    # A list to be populated with async tasks.
    tasks = []

    # Iterate through all json files in the directory.
    for path in pathlist:
        tasks.append(asyncio.ensure_future(write_pokemon_moves(path.name)))

    # Will contain a list of dictionaries containing Pokemons' names and moves
    moves_list = await asyncio.gather(*tasks)

Il s'agit d'un moyen courant d'utiliser du code asynchrone dans Python, souvent utilisé pour des requêtes HTTP, par exemple.

À quoi sert-il ?

Les exemples de ce post utilisant les données des Pokémon n'étaient qu'une excuse pour montrer la fonctionnalité du module aiofiles et la façon dont vous écririez du code pour naviguer dans un répertoire de fichiers à lire et à écrire. Nous espérons que vous pourrez adapter ces exemples de code aux problèmes spécifiques que vous essayez de résoudre afin que les E/S de fichier ne deviennent pas un obstacle dans votre code asynchrone.

Nous n'avons fait qu'effleurer la surface de ce que vous pouvez faire avec aiohttp et asyncio, mais j'espère que cela vous a permis de vous lancer un peu plus facilement dans le monde du Python asynchrone.

J'ai hâte de découvrir ce que vous construisez. N'hésitez pas à me contacter et à partager vos expériences ou à poser des questions.