Gestionnaires de contexte et Python avec instruction – Real Python

By | juin 2, 2021

python pour débutant

le avec en Python est un outil très utile pour gérer correctement les ressources externes dans vos programmes. Il vous permet de profiter de l'existant gestionnaires de contexte pour gérer automatiquement les phases de configuration et de démontage chaque fois que vous avez affaire à des ressources externes ou à des opérations qui nécessitent ces phases.

En plus, le protocole de gestion de contexte vous permet de créer vos propres gestionnaires de contexte afin de personnaliser la façon dont vous gérez les ressources système. Alors, quel est le avec déclaration bonne pour?

Dans ce tutoriel, vous apprendrez :

  • Qu'est-ce que le Python avec déclaration est pour et comment l'utiliser
  • Qu'est-ce que le protocole de gestion de contexte est
  • Comment mettre en œuvre votre propre gestionnaires de contexte

Avec cette connaissance, vous écrirez un code plus expressif et éviterez les fuites de ressources dans vos programmes. le avec L'instruction vous aide à implémenter certains modèles de gestion des ressources courants en faisant abstraction de leurs fonctionnalités et en leur permettant d'être factorisés et réutilisés.

Gestion des ressources en Python

Un problème courant auquel vous serez confronté dans la programmation est de savoir comment gérer correctement ressources externes, tels que les fichiers, les verrous et les connexions réseau. Parfois, un programme conservera ces ressources pour toujours, même si vous n'en avez plus besoin. Ce type de problème est appelé fuite de mémoire car la mémoire disponible est réduite chaque fois que vous créez et ouvrez une nouvelle instance d'une ressource donnée sans en fermer une existante.

La bonne gestion des ressources est souvent un problème délicat. Cela nécessite à la fois un mettre en place phase et une démolir phase. Cette dernière phase vous oblige à effectuer certaines actions de nettoyage, telles que la fermeture d'un fichier, la libération d'un verrou ou la fermeture d'une connexion réseau. Si vous oubliez d'effectuer ces actions de nettoyage, votre application maintient la ressource en vie. Cela pourrait compromettre des ressources système précieuses, telles que la mémoire et la bande passante du réseau.

Par exemple, un problème courant qui peut survenir lorsque les développeurs travaillent avec des bases de données est lorsqu'un programme continue de créer de nouvelles connexions sans les libérer ou les réutiliser. Dans ce cas, le back-end de la base de données peut cesser d'accepter de nouvelles connexions. Cela peut nécessiter qu'un administrateur se connecte et supprime manuellement ces connexions obsolètes pour rendre la base de données utilisable à nouveau.

Un autre problème fréquent apparaît lorsque les développeurs travaillent avec des fichiers. L'écriture de texte dans des fichiers est généralement une opération mise en mémoire tampon. Cela signifie que l'appel .écrivez() sur un fichier n'entraînera pas immédiatement l'écriture de texte dans le fichier physique mais dans un tampon temporaire. Parfois, lorsque le tampon n'est pas plein et que les développeurs oublient d'appeler .Fermer(), une partie des données peut être perdue à jamais.

Une autre possibilité est que votre application rencontre des erreurs ou des exceptions qui font que le flux de contrôle contourne le code responsable de la libération de la ressource disponible. Voici un exemple dans lequel vous utilisez ouvert() pour écrire du texte dans un fichier :

déposer = ouvert("bonjour.txt", "w")
déposer.écrivez("Bonjour le monde!")
déposer.Fermer()

Cette implémentation ne garantit pas que le fichier sera fermé si une exception se produit pendant la .écrivez() appel. Dans ce cas, le code n'appellera jamais .Fermer(), et par conséquent, votre programme peut divulguer un descripteur de fichier.

En Python, vous pouvez utiliser deux approches générales pour gérer la gestion des ressources. Vous pouvez encapsuler votre code dans :

  1. UNE essayerfinalement construction
  2. UNE avec construction

La première approche est assez générale et vous permet de fournir du code d'installation et de démontage pour gérer tout type de ressource. Cependant, c'est un peu bavard. Et si vous oubliez des actions de nettoyage ?

La deuxième approche fournit un moyen simple de fournir et de réutiliser le code de configuration et de démontage. Dans ce cas, vous aurez la limitation que le avec L'instruction ne fonctionne qu'avec les gestionnaires de contexte. Dans les deux sections suivantes, vous apprendrez à utiliser les deux approches dans votre code.

le essayerfinalement Approcher

Travailler avec des fichiers est probablement l'exemple le plus courant de gestion des ressources en programmation. En Python, vous pouvez utiliser un essayerfinalement instruction pour gérer correctement l'ouverture et la fermeture des fichiers :

# Ouvrez le fichier en toute sécurité
déposer = ouvert("bonjour.txt", "w")

essayer:
    déposer.écrivez("Bonjour le monde!")
finalement:
    # Assurez-vous de fermer le fichier après l'avoir utilisé
    déposer.Fermer()

Dans cet exemple, vous devez ouvrir le fichier en toute sécurité bonjour.txt, ce que vous pouvez faire en enveloppant l'appel vers ouvert() dans un essayersauf déclaration. Plus tard, lorsque vous essayez d'écrire à déposer, les finalement clause garantira que déposer est correctement fermé, même si une exception survient lors de l'appel à .écrivez() dans le essayer clause. Vous pouvez utiliser ce modèle pour gérer la logique de configuration et de démontage lorsque vous gérez des ressources externes en Python.

le essayer block dans l'exemple ci-dessus peut potentiellement déclencher des exceptions, telles que AttributeError ou alors NameError. Vous pouvez gérer ces exceptions dans un sauf clause comme celle-ci :

# Ouvrez le fichier en toute sécurité
déposer = ouvert("bonjour.txt", "w")

essayer:
    déposer.écrivez("Bonjour le monde!")
sauf Exception comme e:
    imprimer(F"Une erreur s'est produite lors de l'écriture dans le fichier : e")
finalement:
    # Assurez-vous de fermer le fichier après l'avoir utilisé
    déposer.Fermer()

Dans cet exemple, vous interceptez toutes les exceptions potentielles qui peuvent se produire lors de l'écriture dans le fichier. Dans des situations réelles, vous devez utiliser un type d'exception spécifique au lieu du général Exception pour empêcher les erreurs inconnues de passer en silence.

le avec Approche de l'énoncé

Le Python avec déclaration crée un contexte d'exécution qui vous permet d'exécuter un groupe d'instructions sous le contrôle d'un gestionnaire de contexte. PEP 343 a ajouté le avec déclaration pour permettre de factoriser les cas d'utilisation standard du essayerfinalement déclaration.

Par rapport au traditionnel essayerfinalement construit, le avec peut rendre votre code plus clair, plus sûr et réutilisable. De nombreuses classes de la bibliothèque standard prennent en charge le avec déclaration. Un exemple classique en est ouvert(), qui vous permet de travailler avec des objets fichier en utilisant avec.

Pour écrire un avec instruction, vous devez utiliser la syntaxe générale suivante :

avec expression comme var_cible:
    faire quelque chose(var_cible)

L'objet gestionnaire de contexte résulte de l'évaluation de la expression après avec. Autrement dit, expression doit retourner un objet qui implémente le protocole de gestion de contexte. Ce protocole se compose de deux méthodes spéciales :

  1. .__Entrer__() est appelé par le avec pour entrer dans le contexte d'exécution.
  2. .__sortir__() est appelé lorsque l'exécution quitte le avec bloc de code.

le comme le spécificateur est facultatif. Si vous fournissez un var_cible avec comme, puis la valeur de retour de l'appel .__Entrer__() sur l'objet gestionnaire de contexte est lié à cette variable.

Voici comment le avec L'instruction se poursuit lorsque Python l'exécute :

  1. Appel expression pour obtenir un gestionnaire de contexte.
  2. Stocker le gestionnaire de contexte .__Entrer__() et .__sortir__() méthodes pour une utilisation ultérieure.
  3. Appel .__Entrer__() sur le gestionnaire de contexte et lier sa valeur de retour à var_cible si fourni.
  4. Exécuter le avec bloc de code.
  5. Appel .__sortir__() sur le gestionnaire de contexte lorsque le avec le bloc de code se termine.

Dans ce cas, .__Entrer__(), fournit généralement le code de configuration. le avec est une instruction composée qui démarre un bloc de code, comme une instruction conditionnelle ou un pour boucle. À l'intérieur de ce bloc de code, vous pouvez exécuter plusieurs instructions. En règle générale, vous utilisez le avec bloc de code à manipuler var_cible le cas échéant.

Une fois la avec le bloc de code se termine, .__sortir__() est appelé. Cette méthode fournit généralement la logique de démontage ou le code de nettoyage, comme l'appel .Fermer() sur un objet fichier ouvert. C'est pourquoi le avec déclaration est si utile. Cela facilite l'acquisition et la libération des ressources.

Voici comment ouvrir votre bonjour.txt fichier pour l'écriture à l'aide du avec déclaration:

avec ouvert("bonjour.txt", mode="w") comme déposer:
    déposer.écrivez("Bonjour le monde!")

Lorsque vous exécutez ce avec déclaration, ouvert() renvoie un io.TextIOBase objet. Cet objet est aussi un gestionnaire de contexte, donc le avec appels de déclaration .__Entrer__() et attribue sa valeur de retour à déposer. Ensuite, vous pouvez manipuler le fichier à l'intérieur du avec bloc de code. Lorsque le bloc se termine, .__sortir__() est automatiquement appelé et ferme le fichier pour vous, même si une exception est levée dans le avec bloquer.

Cette avec la construction est plus courte que sa essayerfinalement alternative, mais c'est aussi moins général, comme vous l'avez déjà vu. Vous ne pouvez utiliser que le avec avec des objets qui prennent en charge le protocole de gestion de contexte, alors que essayerfinalement vous permet d'effectuer des actions de nettoyage pour des objets arbitraires sans avoir besoin de prendre en charge le protocole de gestion de contexte.

Dans Python 3.1 et versions ultérieures, le avec L'instruction prend en charge plusieurs gestionnaires de contexte. Vous pouvez fournir n'importe quel nombre de gestionnaires de contexte séparés par des virgules :

avec UNE() comme une, B() comme b:
    passe

Cela fonctionne comme imbriqué avec déclarations mais sans imbrication. Cela peut être utile lorsque vous devez ouvrir deux fichiers à la fois, le premier pour la lecture et le second pour l'écriture :

avec ouvert("entrée.txt") comme dans le fichier, ouvert("sortie.txt", "w") comme out_file:
    # Lire le contenu de input.txt
    # Transformez le contenu
    # Écrivez le contenu transformé dans output.txt
    passe

Dans cet exemple, vous pouvez ajouter du code pour lire et transformer le contenu de entrée.txt. Ensuite, vous écrivez le résultat final sur sortie.txt dans le même bloc de code.

Utilisation de plusieurs gestionnaires de contexte dans un seul avec a un inconvénient, cependant. Si vous utilisez cette fonctionnalité, vous dépasserez probablement votre limite de longueur de ligne. Pour contourner ce problème, vous devez utiliser des barres obliques inverses () pour la continuation de ligne, vous pourriez donc vous retrouver avec un résultat final moche.

le avec peut rendre le code qui traite des ressources système plus lisible, réutilisable et concis, pour ne pas dire plus sûr. Cela permet d'éviter les bogues et les fuites en rendant presque impossible d'oublier le nettoyage, la fermeture et la libération d'une ressource une fois que vous en avez terminé.

Utilisant avec vous permet d'abstraire la plupart de la logique de gestion des ressources. Au lieu d'avoir à écrire un texte explicite essayerfinalement déclaration avec le code d'installation et de démontage à chaque fois, avec s'en occupe pour vous et évite les répétitions.

Utiliser le Python avec Déclaration

Tant que les développeurs Python ont intégré le avec déclaration dans leur pratique de codage, l'outil s'est avéré avoir plusieurs cas d'utilisation précieux. De plus en plus d'objets de la bibliothèque standard Python prennent désormais en charge le protocole de gestion de contexte afin que vous puissiez les utiliser dans un avec déclaration.

Dans cette section, vous allez coder quelques exemples qui montrent comment utiliser le avec avec plusieurs classes à la fois dans la bibliothèque standard et dans les bibliothèques tierces.

Travailler avec des fichiers

Jusqu'à présent, vous avez utilisé ouvert() fournir un gestionnaire de contexte et manipuler des fichiers dans un avec construction. Ouverture de fichiers à l'aide du avec est généralement recommandée car elle garantit que les descripteurs de fichiers ouverts sont automatiquement fermés une fois que le flux d'exécution a quitté le avec bloc de code.

Comme vous l'avez vu précédemment, la façon la plus courante d'ouvrir un fichier en utilisant avec est à travers le intégré ouvert():

avec ouvert("bonjour.txt", mode="w") comme déposer:
    déposer.écrivez("Bonjour le monde!")

Dans ce cas, puisque le gestionnaire de contexte ferme le fichier après avoir quitté le avec bloc de code, une erreur courante peut être la suivante :

>>>

>>> déposer = ouvert("bonjour.txt", mode="w")

>>> avec déposer:
...     déposer.écrivez("Bonjour le monde!")
...
13

>>> avec déposer:
...     déposer.écrivez("Bienvenue dans le vrai Python !")
...
Traceback (appel le plus récent en dernier) :
  Déposer "", ligne 1, dans 
Erreur de valeur: Opération d'E/S sur fichier fermé.

La première avec écrit avec succès "Bonjour le monde!" dans bonjour.txt. Noter que .écrivez() renvoie le nombre d'octets écrits dans le fichier, 13. Quand tu essaies de courir une seconde avec, cependant, vous obtenez un Erreur de valeur parce que votre déposer est déjà fermé.

Une autre façon d'utiliser le avec instruction pour ouvrir et gérer les fichiers est en utilisant pathlib.Path.open():

>>>

>>> importer pathlib

>>> chemin du fichier = pathlib.Chemin("bonjour.txt")

>>> avec chemin du fichier.ouvert("w") comme déposer:
...     déposer.écrivez("Bonjour le monde!")
...
13

Chemin est une classe qui représente des chemins concrets vers des fichiers physiques sur votre ordinateur. Appel .ouvert() sur un Chemin objet qui pointe vers un fichier physique l'ouvre comme ouvert() ferait. Donc, Chemin.open() fonctionne de la même manière que ouvert(), mais le chemin du fichier est automatiquement fourni par le Chemin objet sur lequel vous appelez la méthode.

Depuis pathlib fournit un moyen élégant, simple et Pythonic de manipuler les chemins du système de fichiers, vous devriez envisager d'utiliser Chemin.open() dans votre avec déclarations en tant que meilleure pratique en Python.

Enfin, chaque fois que vous chargez un fichier externe, votre programme doit rechercher d'éventuels problèmes, tels qu'un fichier manquant, un accès en écriture et en lecture, etc. Voici un modèle général que vous devriez envisager d'utiliser lorsque vous travaillez avec des fichiers :

importer pathlib
importer enregistrement

chemin du fichier = pathlib.Chemin("bonjour.txt")

essayer:
    avec chemin du fichier.ouvert(mode="w") comme déposer:
        déposer.écrivez("Bonjour le monde!")
sauf OSError comme Erreur:
    enregistrement.Erreur("Écrire au dossier %s    a échoué en raison de : %s", chemin du fichier, Erreur)

Dans cet exemple, vous enveloppez le avec déclaration dans un essayersauf déclaration. Si un OSError survient lors de l'exécution de avec, alors tu utilises enregistrement pour consigner l'erreur avec un message convivial et descriptif.

Parcourir les répertoires

le système d'exploitation module fournit une fonction appelée scandir(), qui renvoie un itérateur sur os.DirEntry objets correspondant aux entrées d'un répertoire donné. Cette fonction est spécialement conçue pour fournir des performances optimales lorsque vous parcourez une structure de répertoires.

Un appel à scandir() avec le chemin d'accès à un répertoire donné en argument renvoie un itérateur qui prend en charge le protocole de gestion de contexte :

>>>

>>> importer système d'exploitation

>>> avec système d'exploitation.scandir(".") comme entrées:
...     pour entrée dans entrées:
...         imprimer(entrée.Nom, "->", entrée.statistique().st_taille, "octets")
...
Documents -> 4096 octets
Vidéos -> 12288 octets
Bureau -> 4096 octets
DevSpace -> 4096 octets
.profile -> 807 octets
Modèles -> 4096 octets
Images -> 12288 octets
Public -> 4096 octets
Téléchargements -> 4096 octets

Dans cet exemple, vous écrivez un avec déclaration avec os.scandir() en tant que fournisseur de gestionnaire de contexte. Ensuite, vous parcourez les entrées du répertoire sélectionné (".") et imprimez leur nom et leur taille à l'écran. Dans ce cas, .__sortir__() appels scandir.close() pour fermer l'itérateur et libérer les ressources acquises. Notez que si vous l'exécutez sur votre machine, vous obtiendrez une sortie différente en fonction du contenu de votre répertoire actuel.

Effectuer des calculs de haute précision

Contrairement aux nombres à virgule flottante intégrés, le décimal module fournit un moyen d'ajuster la précision à utiliser dans un calcul donné qui implique Décimal Nombres. La précision par défaut est 28 endroits, mais vous pouvez le modifier pour répondre aux exigences de votre problème. Un moyen rapide d'effectuer des calculs avec une précision personnalisée consiste à utiliser contexte local() de décimal:

>>>

>>> de décimal importer Décimal, contexte local

>>> avec contexte local() comme ctx:
...     ctx.prec = 42
...     Décimal("1") / Décimal("42")
...
Décimal('0.0238095238095238095238095238095238095238095')

>>> Décimal("1") / Décimal("42")
Décimal('0.02380952380952380952380952381')

Ici, contexte local() fournit un gestionnaire de contexte qui crée un contexte décimal local et vous permet d'effectuer des calculs avec une précision personnalisée. Dans le avec bloc de code, vous devez définir .prec à la nouvelle précision que vous souhaitez utiliser, qui est 42 endroits dans l'exemple ci-dessus. Quand le avec le bloc de code se termine, la précision est réinitialisée à sa valeur par défaut, 28 des endroits.

Gestion des verrous dans les programmes multithreads

Un autre bon exemple d'utilisation du avec déclaration efficace dans la bibliothèque standard Python est filetage.Verrouillage. Cette classe fournit un verrou primitif pour empêcher plusieurs threads de modifier une ressource partagée en même temps dans une application multithread.

Vous pouvez utiliser un Fermer à clé objet en tant que gestionnaire de contexte dans un avec pour acquérir et libérer automatiquement un verrou donné. Par exemple, supposons que vous deviez protéger le solde d'un compte bancaire :

importer enfilage

balance_lock = enfilage.Fermer à clé()

# Utilisez le modèle try ... finally
balance_lock.acquérir()
essayer:
    # Mettez à jour le solde du compte ici ...
finalement:
    balance_lock.Libération()

# Utilisez le modèle avec
avec balance_lock:
    # Mettez à jour le solde du compte ici ...

le avec L'instruction dans le deuxième exemple acquiert et libère automatiquement un verrou lorsque le flux d'exécution entre et sort de l'instruction. De cette façon, vous pouvez vous concentrer sur ce qui compte vraiment dans votre code et oublier ces opérations répétitives.

Dans cet exemple, le verrou du avec L'instruction crée une zone protégée connue sous le nom de section critique, qui empêche l'accès simultané au solde du compte.

Tester les exceptions avec pytest

Jusqu'à présent, vous avez codé quelques exemples à l'aide de gestionnaires de contexte disponibles dans la bibliothèque standard Python. Cependant, plusieurs bibliothèques tierces incluent des objets qui prennent en charge le protocole de gestion de contexte.

Disons que vous testez votre code avec pytest. Certaines de vos fonctions et blocs de code lèvent des exceptions dans certaines situations, et vous souhaitez tester ces cas. Pour ce faire, vous pouvez utiliser pytest.raises(). Cette fonction vous permet d'affirmer qu'un bloc de code ou un appel de fonction lève une exception donnée.

Depuis pytest.raises() fournit un gestionnaire de contexte, vous pouvez l'utiliser dans un avec déclaration comme celle-ci :

>>>

>>> importer pytest

>>> 1 / 0
Traceback (appel le plus récent en dernier) :
  Déposer "", ligne 1, dans 
ZeroDivisionError: division par zéro

>>> avec pytest.soulève(ZeroDivisionError):
...     1 / 0
...

>>> favoris = "fruit": "Pomme", "animaux": "chien"
>>> favoris[[[["voiture"]
Traceback (appel le plus récent en dernier) :
  Déposer "", ligne 1, dans 
KeyError: 'voiture'

>>> avec pytest.soulève(KeyError):
...     favoris[[[["voiture"]
...

Dans le premier exemple, vous utilisez pytest.raises() pour capturer le ZeroDivisionError que l'expression dix soulève. Le deuxième exemple utilise la fonction pour capturer le KeyError qui est déclenché lorsque vous accédez à une clé qui n'existe pas dans un dictionnaire donné.

Si votre fonction ou bloc de code ne lève pas l'exception attendue, alors pytest.raises() lève une exception d'échec :

>>>

>>> importer pytest

>>> avec pytest.soulève(ZeroDivisionError):
...     4 / 2
...
2.0
Traceback (appel le plus récent en dernier) :
  ...
Manqué: N'A PAS AUGMENTÉ 

Une autre fonctionnalité intéressante de pytest.raises() est que vous pouvez spécifier une variable cible pour inspecter l'exception déclenchée. Par exemple, si vous souhaitez vérifier le message d'erreur, vous pouvez faire quelque chose comme ceci :

>>>

>>> avec pytest.soulève(ZeroDivisionError) comme sauf:
...     1 / 0
...
>>> affirmer str(sauf.valeur) == "division par zéro"

Vous pouvez utiliser tous ces pytest.raises() fonctionnalités pour capturer les exceptions que vous soulevez à partir de vos fonctions et de votre bloc de code. Il s'agit d'un outil sympa et utile que vous pouvez intégrer à votre stratégie de test actuelle.

Résumant le avec Avantages de la déclaration

Pour résumer ce que vous avez appris jusqu'à présent, voici une liste non exhaustive des avantages généraux de l'utilisation de Python avec déclaration dans votre code:

  • Fait du la gestion des ressources plus sûr que son équivalent essayerfinalement déclarations
  • Encapsule les utilisations standard de essayerfinalement déclarations dans gestionnaires de contexte
  • Permet de réutiliser le code qui gère automatiquement le mettre en place et démolir phases d'une opération donnée
  • Aide à éviter fuites de ressources

En utilisant le avec peut améliorer la qualité générale de votre code et le rendre plus sûr en évitant les problèmes de fuite de ressources.

En utilisant le asynchrone avec Déclaration

le avec instruction a également une version asynchrone, asynchrone avec. Vous pouvez l'utiliser pour écrire des gestionnaires de contexte qui dépendent du code asynchrone. Il est assez courant de voir asynchrone avec dans ce type de code, car de nombreuses opérations d'E/S impliquent des phases de configuration et de démontage.

Par exemple, supposons que vous ayez besoin de coder une fonction asynchrone pour vérifier si un site donné est en ligne. Pour ce faire, vous pouvez utiliser aiohttp, asynchrone, et asynchrone avec comme ça:

    1# site_checker_v0.py
    2
    3importer aiohttp
    4importer asynchrone
    5
    6asynchrone déf Chèque(URL):
    7    asynchrone avec aiohttp.Session Client() comme session:
    8        asynchrone avec session.obtenir(URL) comme réponse:
    9            imprimer(F"URL: état -> réponse.statut")
dix            html = attendre réponse.texte()
11            imprimer(F"URL: tapez -> html[:[:[:[:17].déshabiller()")
12
13asynchrone déf principale():
14    attendre asynchrone.rassembler(
15        Chèque("https://realpython.com"),
16        Chèque("https://pycoders.com"),
17    )
18
19asynchrone.Cours(principale())

Voici ce que fait ce script :

  • Ligne 3 importations aiohttp, qui fournit un client et un serveur HTTP asynchrones pour asynchrone et Python. Noter que aiohttp est un package tiers que vous pouvez installer en exécutant python -m pip installer aiohttp sur votre ligne de commande.
  • Ligne 4 importations asynchrone, qui vous permet d'écrire du code concurrent en utilisant le asynchrone et attendre syntaxe.
  • Ligne 6 définit Chèque() en tant que fonction asynchrone utilisant le asynchrone mot-clé.

À l'intérieur Chèque(), vous définissez deux imbriqués asynchrone avec déclarations :

  • Ligne 7 définit un extérieur asynchrone avec qui instancie aiohttp.ClientSession() pour obtenir un gestionnaire de contexte. Il stocke l'objet renvoyé dans session.
  • Ligne 8 définit un intérieur asynchrone avec déclaration qui appelle .obtenir() au session utilisant URL comme argument. Cela crée un deuxième gestionnaire de contexte et renvoie un réponse.
  • Ligne 9 imprime le code d'état de la réponse pour le URL à portée de main.
  • Ligne 10 lance un appel en attente à .texte() au réponse et enregistre le résultat dans html.
  • Ligne 11 imprime le site URL et son type de document, type de document.
  • Ligne 13 définit le script principale() fonction, qui est aussi une coroutine.
  • Ligne 14 appels rassembler() de asynchrone. Cette fonction exécute simultanément des objets en attente dans une séquence. Dans cet exemple, rassembler() exécute deux instances de Chèque() avec une URL différente pour chacun.
  • Ligne 19 s'exécute principale() utilisant asyncio.run(). Cette fonction crée un nouveau asynchrone boucle événementielle et la ferme à la fin de l'opération.

Si vous exécutez ce script à partir de votre ligne de commande, vous obtenez une sortie semblable à la suivante :

$ python site_checker_v0.py
https://realpython.com : statut -> 200
https://pycoders.com : état -> 200
https://pycoders.com : tapez -> 
https://realpython.com : tapez -> 

Frais! Votre script fonctionne et vous confirmez que les deux sites sont actuellement disponibles. Vous récupérez également les informations concernant le type de document à partir de la page d'accueil de chaque site.

le asynchrone avec l'instruction fonctionne de manière similaire à la normale avec déclaration, mais cela nécessite une gestionnaire de contexte asynchrone. En d'autres termes, il a besoin d'un gestionnaire de contexte capable de suspendre l'exécution dans ses méthodes d'entrée et de sortie. Les gestionnaires de contexte asynchrones implémentent les méthodes spéciales .__aenter__() et .__aexit__(), qui correspondent à .__Entrer__() et .__sortir__() dans un gestionnaire de contexte régulier.

le async avec ctx_mgr construction utilise implicitement attendre ctx_mgr.__aenter__() en entrant dans le contexte et attendre ctx_mgr.__aexit__() en le sortant. Cela atteint asynchrone le comportement du gestionnaire de contexte de manière transparente.

Création de gestionnaires de contexte personnalisés

Vous avez déjà travaillé avec des gestionnaires de contexte de la bibliothèque standard et des bibliothèques tierces. Il n'y a rien de spécial ou de magique ouvert(), filetage.Verrouillage, decimal.localcontext(), ou les autres. Ils renvoient simplement des objets qui implémentent le protocole de gestion de contexte.

Vous pouvez fournir la même fonctionnalité en implémentant à la fois le .__Entrer__() et le .__sortir__() méthodes spéciales dans votre basé sur la classe gestionnaires de contexte. Vous pouvez également créer sur mesure basé sur la fonction gestionnaires de contexte utilisant le contextlib.contextmanager décorateur de la bibliothèque standard et une fonction génératrice codée de manière appropriée.

En général, les gestionnaires de contexte et les avec ne se limitent pas à la gestion des ressources. Ils vous permettent de fournir et de réutiliser le code d'installation et de démontage commun. En d'autres termes, avec les gestionnaires de contexte, vous pouvez effectuer n'importe quelle paire d'opérations à effectuer avant que et après une autre opération ou procédure, telle que :

  • Ouvrir et fermer
  • Verrouiller et libérer
  • Modifier et réinitialiser
  • Créer et supprimer
  • Entrer et sortir
  • Démarrer et arrêter
  • Installation et démontage

Vous pouvez fournir du code pour gérer en toute sécurité l'une de ces paires d'opérations dans un gestionnaire de contexte. Ensuite, vous pouvez réutiliser ce gestionnaire de contexte dans avec déclarations tout au long de votre code. Cela évite les erreurs et réduit le code passe-partout répétitif. Cela rend également vos API plus sûres, plus propres et plus conviviales.

Dans les deux sections suivantes, vous apprendrez les bases de la création de gestionnaires de contexte basés sur les classes et les fonctions.

Codage des gestionnaires de contexte basés sur les classes

Pour mettre en œuvre le protocole de gestion de contexte et créer basé sur la classe gestionnaires de contexte, vous devez ajouter à la fois les .__Entrer__() et le __sortir__() méthodes spéciales à vos classes. Le tableau ci-dessous résume le fonctionnement de ces méthodes, les arguments qu'elles prennent et la logique que vous pouvez y mettre :

Méthode La description
.__enter__(self) Cette méthode gère la logique de configuration et est appelée lors de la saisie d'un nouveau avec le contexte. Sa valeur de retour est liée au avec variable cible.
.__exit__(self, exc_type, exc_value, exc_tb) Cette méthode gère la logique de démontage et est appelée lorsque le flux d'exécution quitte le avec le contexte. Si une exception se produit, alors exc_type, valeur_exc, et exc_tb contiennent respectivement le type d'exception, la valeur et les informations de traçabilité.

Quand le avec l'instruction s'exécute, elle appelle .__Entrer__() sur l'objet gestionnaire de contexte pour signaler que vous entrez dans un nouveau contexte d'exécution. Si vous fournissez une variable cible avec le comme spécificateur, puis la valeur de retour de .__Entrer__() est affecté à cette variable.

Lorsque le flux d'exécution quitte le contexte, .__sortir__() est appelé. Si aucune exception ne se produit dans le avec bloc de code, puis les trois derniers arguments à .__sortir__() sont réglés sur Rien. Sinon, ils contiennent le type, la valeur et le retraçage associés à l'exception à portée de main.

Si la .__sortir__() la méthode renvoie Vrai, alors toute exception qui se produit dans le avec bloc est avalé et l'exécution continue à l'instruction suivante après avec. Si .__sortir__() Retour Faux, les exceptions sont propagées hors du contexte. C'est également le comportement par défaut lorsque la méthode ne renvoie rien de manière explicite. Vous pouvez tirer parti de cette fonctionnalité pour encapsuler la gestion des exceptions dans le gestionnaire de contexte.

Écrire un exemple de gestionnaire de contexte basé sur les classes

Voici un exemple de gestionnaire de contexte basé sur les classes qui implémente les deux méthodes, .__Entrer__() et .__sortir__(). Il montre également comment Python les appelle dans un avec construction:

>>>

>>> classer BonjourContextManager:
...     déf __Entrer__(soi):
...         imprimer("Entrer dans le contexte...")
...         revenir "Bonjour le monde!"
...     déf __sortir__(soi, exc_type, valeur_exc, exc_tb):
...         imprimer("Quitter le contexte...")
...         imprimer(exc_type, valeur_exc, exc_tb, SEP="n")
...

>>> avec BonjourContextManager() comme Bonjour:
...     imprimer(Bonjour)
...
Entrer dans le contexte...
Bonjour le monde!
Quitter le contexte...
Rien
Rien
Rien

BonjourContextManager met en œuvre à la fois .__Entrer__() et .__sortir__(). Dans .__Entrer__(), vous imprimez d'abord un message pour signaler que le flux d'exécution entre dans un nouveau contexte. Ensuite, vous retournez le "Bonjour le monde!" chaîne. Dans .__sortir__(), vous imprimez un message pour signaler que le flux d'exécution quitte le contexte. Vous imprimez également le contenu de ses trois arguments.

Quand le avec l'instruction s'exécute, Python crée une nouvelle instance de BonjourContextManager et appelle son .__Entrer__() méthode. Vous le savez parce que vous obtenez Entrer dans le contexte... imprimé à l'écran.

Ensuite, Python exécute le avec bloc de code, qui imprime Bonjour à l'écran. Noter que Bonjour détient la valeur de retour de .__Entrer__().

Lorsque le flux d'exécution quitte le avec bloc de code, appels Python .__sortir__(). Vous le savez parce que vous obtenez Quitter le contexte... imprimé sur votre écran. La dernière ligne de la sortie confirme que les trois arguments à .__sortir__() sont réglés sur Rien.

Maintenant, que se passe-t-il si une exception se produit lors de l'exécution du avec bloquer? Allez-y et écrivez ce qui suit avec déclaration:

>>>

>>> avec BonjourContextManager() comme Bonjour:
...     imprimer(Bonjour)
...     Bonjour[[[[100]
...
Entrer dans le contexte...
Bonjour le monde!
Quitter le contexte...

index de chaîne hors limites

Traceback (appel le plus récent en dernier) :
  Déposer "", ligne 3, dans 
IndexError: index de chaîne hors limites

Dans ce cas, vous essayez de récupérer la valeur à l'index 100 dans la chaîne "Bonjour le monde!". Cela soulève un IndexError, et les arguments de .__sortir__() sont définis comme suit :

  • exc_type is the exception class, IndexError.
  • exc_value is the exception instance.
  • exc_tb is the traceback object.

This behavior is quite useful when you want to encapsulate the exception handling in your context managers.

Handling Exceptions in a Context Manager

As an example of encapsulating exception handling in a context manager, say you expect IndexError to be the most common exception when you’re working with HelloContextManager. You might want to handle that exception in the context manager so you don’t have to repeat the exception-handling code in every with code block. In that case, you can do something like this:

# exc_handling.py

class HelloContextManager:
    def __enter__(self):
        imprimer("Entering the context...")
        return "Hello, World!"

    def __exit__(self, exc_type, exc_value, exc_tb):
        imprimer("Leaving the context...")
        if isinstance(exc_value, IndexError):
            # Handle IndexError here...
            imprimer(F"An exception occurred in your with block: exc_type")
            imprimer(F"Exception message: exc_value")
            return True

with HelloContextManager() as Bonjour:
    imprimer(Bonjour)
    Bonjour[[[[100]

imprimer("Continue normally from here...")

Dans .__exit__(), you check if exc_value is an instance of IndexError. If so, then you print a couple of informative messages and finally return with True. Returning a truthy value makes it possible to swallow the exception and continue the normal execution after the with code block.

In this example, if no IndexError occurs, then the method returns None and the exception propagates out. However, if you want to be more explicit, then you can return False from outside the if block.

If you run exc_handling.py from your command line, then you get the following output:

$ python exc_handling.py
Entering the context...
Hello, World!
Leaving the context...
An exception occurred in your with block: 
Exception message: string index out of range
Continue normally from here...

HelloContextManager is now able to handle IndexError exceptions that occur in the with code block. Since you return True when an IndexError occurs, the flow of execution continues in the next line, right after exiting the with code block.

Opening Files for Writing: First Version

Now that you know how to implement the context management protocol, you can get a sense of what this would look like by coding a practical example. Here’s how you can take advantage of open() to create a context manager that opens files for writing:

# writable.py

class WritableFile:
    def __init__(self, file_path):
        self.file_path = file_path

    def __enter__(self):
        self.file_obj = open(self.file_path, mode="w")
        return self.file_obj

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file_obj:
            self.file_obj.close()

WritableFile implements the context management protocol and supports the with statement, just like the original open() does, but it always opens the file for writing using the "w" mode. Here’s how you can use your new context manager:

>>>

>>> de writable import WritableFile

>>> with WritableFile("hello.txt") as déposer:
...    déposer.write("Hello, World!")
...

After running this code, your hello.txt file contains the "Hello, World!" chaîne. As an exercise, you can write a complementary context manager that opens files for reading, but using pathlib functionalities. Go ahead and give it a shot!

Redirecting the Standard Output

A subtle detail to consider when you’re writing your own context managers is that sometimes you don’t have a useful object to return from .__enter__() and therefore to assign to the with target variable. In those cases, you can return None explicitly or you can just rely on Python’s implicit return value, which is None as well.

For example, say you need to temporarily redirect the standard output, sys.stdout, to a given file on your disk. To do this, you can create a context manager like this:

# redirect.py

import sys

class RedirectedStdout:
    def __init__(self, new_output):
        self.new_output = new_output

    def __enter__(self):
        self.saved_output = sys.stdout
        sys.stdout = self.new_output

    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout = self.saved_output

This context manager takes a file object through its constructor. Dans .__enter__(), you reassign the standard output, sys.stdout, to an instance attribute to avoid losing the reference to it. Then you reassign the standard output to point to the file on your disk. Dans .__exit__(), you just restore the standard output to its original value.

To use RedirectedStdout, you can do something like this:

>>>

>>> de redirect import RedirectedStdout

>>> with open("hello.txt", "w") as déposer:
...     with RedirectedStdout(déposer):
...         imprimer("Hello, World!")
...     imprimer("Back to the standard output...")
...
Back to the standard output...

The outer with statement in this example provides the file object that you’re going to use as your new output, hello.txt. The inner with temporarily redirects the standard output to hello.txt, so the first call to print() writes directly to that file instead of printing "Hello, World!" on your screen. Note that when you leave the inner with code block, the standard output goes back to its original value.

RedirectedStdout is a quick example of a context manager that doesn’t have a useful value to return from .__enter__(). However, if you’re only redirecting the print() output, you can get the same functionality without the need for coding a context manager. You just need to provide a déposer argument to print() like this:

>>>

>>> with open("hello.txt", "w") as déposer:
...     imprimer("Hello, World!", déposer=déposer)
...

In this examples, print() takes your hello.txt file as an argument. This causes print() to write directly into the physical file on your disk instead of printing "Hello, World!" to your screen.

Measuring Execution Time

Just like every other class, a context manager can encapsulate some internal state. The following example shows how to create a stateful context manager to measure the execution time of a given code block or function:

# timing.py

de time import perf_counter

class Timer:
    def __enter__(self):
        self.démarrer = perf_counter()
        self.end = 0.0
        return lambda: self.end - self.démarrer

    def __exit__(self, *args):
        self.end = perf_counter()

When you use Timer in a with statement, .__enter__() gets called. This method uses time.perf_counter() to get the time at the beginning of the with code block and stores it in .start. It also initializes .end and returns a lambda function that computes a time delta. In this case, .start holds the initial state or time measurement.

Une fois la with block ends, .__exit__() gets called. The method gets the time at the end of the block and updates the value of .end so that the lambda function can compute the time required to run the with code block.

Here’s how you can use this context manager in your code:

>>>

>>> de time import sleep
>>> de timing import Timer

>>> with Timer() as timer:
...     # Time-consuming code goes here...
...     sleep(0.5)
...

>>> timer()
0.5005456680000862

Avec Timer, you can measure the execution time of any piece of code. In this example, timer holds an instance of the lambda function that computes the time delta, so you need to call timer() to get the final result.

Creating Function-Based Context Managers

Python’s generator functions and the contextlib.contextmanager decorator provide an alternative and convenient way to implement the context management protocol. If you decorate an appropriately coded generator function with @contextmanager, then you get a function-based context manager that automatically provides both required methods, .__enter__() et .__exit__(). This can make your life more pleasant by saving you some boilerplate code.

The general pattern to create a context manager using @contextmanager along with a generator function goes like this:

>>>

>>> de contextlib import contextmanager

>>> @contextmanager
... def hello_context_manager():
...     imprimer("Entering the context...")
...     yield "Hello, World!"
...     imprimer("Leaving the context...")
...

>>> with hello_context_manager() as Bonjour:
...     imprimer(Bonjour)
...
Entering the context...
Hello, World!
Leaving the context...

In this example, you can identify two visible sections in hello_context_manager(). Before the yield statement, you have the setup section. There, you can place the code that acquires the managed resources. Everything before the yield runs when the flow of execution enters the context.

After the yield statement, you have the teardown section, in which you can release the resources and do the cleanup. The code after yield runs at the end of the with block. le yield statement itself provides the object that will be assigned to the with target variable.

This implementation and the one that uses the context management protocol are practically equivalent. Depending on which one you find more readable, you might prefer one over the other. A downside of the function-based implementation is that it requires an understanding of advanced Python topics, such as decorators and generators.

le @contextmanager decorator reduces the boilerplate required to create a context manager. Instead of writing a whole class with .__enter__() et .__exit__() methods, you just need to implement a generator function with a single yield that produces whatever you want .__enter__() to return.

Opening Files for Writing: Second Version

You can use the @contextmanager to reimplement your WritableFile context manager. Here’s what rewriting it with this technique looks like:

>>>

>>> de contextlib import contextmanager

>>> @contextmanager
... def writable_file(file_path):
...     déposer = open(file_path, mode="w")
...     try:
...         yield déposer
...     finally:
...         déposer.close()
...

>>> with writable_file("hello.txt") as déposer:
...     déposer.write("Hello, World!")
...

In this case, writable_file() is a generator function that opens déposer for writing. Then it temporarily suspends its own execution and yields the resource so with can bind it to its target variable. When the flow of execution leaves the with code block, the function continues to execute and closes déposer correctly.

Mocking the Time

As a final example of how to create custom context managers with @contextmanager, say you’re testing a piece of code that works with time measurements. The code uses time.time() to get the current time measurement and do some further computations. Since time measurements vary, you decide to mock time.time() so you can test your code.

Here’s a function-based context manager that can help you do that:

>>>

>>> de contextlib import contextmanager
>>> de time import time

>>> @contextmanager
... def mock_time():
...     global time
...     saved_time = time
...     time = lambda: 42
...     yield
...     time = saved_time
...

>>> with mock_time():
...     imprimer(F"Mocked time: time()")
...
Mocked time: 42

>>> # Back to normal time
>>> time()
1616075222.4410584

Inside mock_time(), you use a global statement to signal that you’re going to modify the global name time. Then you save the original time() function object in saved_time so you can safely restore it later. The next step is to monkey patch time() using a lambda function that always returns the same value, 42.

The bare yield statement specifies that this context manager doesn’t have a useful object to send back to the with target variable for later use. Après yield, you reset the global time to its original content.

When the execution enters the with block, any calls to time() return 42. Once you leave the with code block, calls to time() return the expected current time. That’s it! Now you can test your time-related code.

Writing Good APIs With Context Managers

Context managers are quite flexible, and if you use the with statement creatively, then you can define convenient APIs for your classes, modules, and packages.

For example, what if the resource you wanted to manage is the text indentation level in some kind of report generator application? In that case, you could write code like this:

with Indenter() as indent:
    indent.imprimer("hi!")
    with indent:
        indent.imprimer("hello")
        with indent:
            indent.imprimer("bonjour")
    indent.imprimer("hey")

This almost reads like a domain-specific language (DSL) for indenting text. Also, notice how this code enters and leaves the same context manager multiple times to switch between different indentation levels. Running this code snippet leads to the following output and prints neatly formatted text:

How would you implement a context manager to support this functionality? This could be a great exercise to wrap your head around how context managers work. So, before you check out the implementation below, you might take some time and try to solve this by yourself as a learning exercise.

Ready? Here’s how you might implement this functionality using a context manager class:

class Indenter:
    def __init__(self):
        self.level = -1

    def __enter__(self):
        self.level += 1
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        self.level -= 1

    def imprimer(self, text):
        imprimer("    " * self.level + text)

Here, .__enter__() increments .level par 1 every time the flow of execution enters the context. The method also returns the current instance, self. Dans .__exit__(), you decrease .level so the printed text steps back one indentation level every time you exit the context.

The key point in this example is that returning self de .__enter__() allows you to reuse the same context manager across several nested with statements. This changes the text indentation level every time you enter and leave a given context.

A good exercise for you at this point would be to write a function-based version of this context manager. Go ahead and give it a try!

Creating an Asynchronous Context Manager

To create an asynchronous context manager, you need to define the .__aenter__() et .__aexit__() methods. The script below is a reimplementation of the original script site_checker_v0.py you saw before, but this time you provide a custom asynchronous context manager to wrap the session creation and closing functionalities:

# site_checker_v1.py

import aiohttp
import asyncio

class AsyncSession:
    def __init__(self, url):
        self._url = url

    async def __aenter__(self):
        self.session = aiohttp.ClientSession()
        réponse = await self.session.get(self._url)
        return réponse

    async def __aexit__(self, exc_type, exc_value, exc_tb):
        await self.session.close()

async def check(url):
    async with AsyncSession(url) as réponse:
        imprimer(F"url: status -> réponse.status")
        html = await réponse.text()
        imprimer(F"url: type -> html[:[:[:[:17].strip()")

async def main():
    await asyncio.gather(
        check("https://realpython.com"),
        check("https://pycoders.com"),
    )

asyncio.run(main())

This script works similar to its previous version, site_checker_v0.py. The main difference is that, in this example, you extract the logic of the original outer async with statement and encapsulate it in AsyncSession.

Dans .__aenter__(), you create an aiohttp.ClientSession(), await the .get() response, and finally return the response itself. Dans .__aexit__(), you close the session, which corresponds to the teardown logic in this specific case. Note that .__aenter__() et .__aexit__() must return awaitable objects. In other words, you must define them with async def, which returns a coroutine object that is awaitable by definition.

If you run the script from your command line, then you get an output similar to this:

$ python site_checker_v1.py
https://realpython.com: status -> 200
https://pycoders.com: status -> 200
https://realpython.com: type -> 
https://pycoders.com: type -> 

Génial! Your script works just like its first version. It sends GET requests to both sites concurrently and processes the corresponding responses.

Finally, a common practice when you’re writing asynchronous context managers is to implement the four special methods:

  1. .__aenter__()
  2. .__aexit__()
  3. .__enter__()
  4. .__exit__()

This makes your context manager usable with both variations of with.

Conclusion

The Python with déclaration is a powerful tool when it comes to managing external resources in your programs. Its use cases, however, aren’t limited to resource management. You can use the with statement along with existing and custom context managers to handle the setup and teardown phases of a given process or operation.

The underlying context management protocol allows you to create custom context managers and factor out the setup and teardown logic so you can reuse them in your code.

In this tutorial, you learned:

  • What the Python with déclaration is for and how to use it
  • What the context management protocol is
  • How to implement your own context managers

With this knowledge, you’ll write safe, concise, and expressive code. You’ll also avoid resource leaks in your programs.

[ad_2]