web-dev-qa-db-fra.com

Python __enter__ / __exit__ vs __init__ (ou __new__) / __del__

J'ai cherché et je n'arrive pas à trouver une bonne raison d'utiliser le __enter__/__exit__ De python plutôt que __init__ (Ou __new__?)/__del__.

Je comprends que __enter__/__exit__ Sont destinés à être utilisés avec l'instruction with en tant que gestionnaires de contexte, et l'instruction with est excellente. Mais la contrepartie à cela est que tout code dans ces blocs est seulement exécuté dans ce contexte. En les utilisant au lieu de __init__/__del__ Il semble que je crée un contrat implicite avec les appelants qu'ils doivent utiliser with, mais il n'y a aucun moyen de faire respecter un tel contrat, et le contrat n'est communiqué que via la documentation (ou la lecture du code). Cela semble être une mauvaise idée.

Il semble que j'obtienne le même effet en utilisant __init__/__del__ À l'intérieur d'un bloc with. Mais en les utilisant plutôt que les méthodes de gestion de contexte, mon objet est également utile dans d'autres scénarios.

Alors, quelqu'un peut-il trouver une raison convaincante pour laquelle je voudrais jamais vouloir utiliser les méthodes de gestion de contexte plutôt que les méthodes constructeur/destructeur?

S'il y a un meilleur endroit pour poser une question comme celle-ci, faites-le moi savoir, mais il semble qu'il n'y ait pas beaucoup de bonnes informations à ce sujet.

Suivi:

Cette question était basée sur une hypothèse erronée (mais probablement commune) car j'ai toujours utilisé with pour instancier un nouvel objet, auquel cas __init__/__del__ Se rapprochait très près du même comportement que __enter__/__exit__ (Sauf que vous ne pouvez pas contrôler quand ou si __del__ Sera exécuté, c'est à la collecte des ordures et si le processus est terminé en premier il ne pourra jamais être appelé). Mais si vous utilisez les objets préexistants dans les instructions with ils sont bien sûr très différents.

22
BobDoolittle

Il y a plusieurs différences que vous semblez avoir manquées:

  • Le gestionnaire de contexte a la possibilité de fournir un nouvel objet juste pour le bloc que vous exécutez. Certains gestionnaires de contexte y renvoient simplement self (comme le font les objets fichier), mais, par exemple, les objets de connexion à la base de données peuvent renvoyer un objet curseur lié à la transaction en cours.

  • Les gestionnaires de contexte ne sont pas simplement informés de la fin du contexte, mais également si la sortie a été provoquée par une exception. Il peut alors décider de gérer cet événement ou réagir différemment lors de la sortie. En utilisant à nouveau une connexion à une base de données comme exemple, sur la base d'une exception, vous pouvez soit valider, soit abandonner la transaction.

  • __del__ n'est appelé que lorsque toutes les références à un objet sont supprimées. Cela signifie que vous ne pouvez pas compter sur son appel si vous devez y avoir plusieurs références dont vous pouvez ou non contrôler la durée de vie. Une sortie du gestionnaire de contexte est cependant précisément définie.

  • Les gestionnaires de contexte peuvent être réutilisés , et ils peuvent garder l'état. La connexion à la base de données à nouveau; vous le créez une fois, puis vous l'utilisez comme gestionnaire de contexte encore et encore, et cela gardera cette connexion ouverte. Il n'est pas nécessaire de créer un nouvel objet à chaque fois pour cela.

    Ceci est important pour les verrous de fil, par exemple; vous avez pour conserver l'état afin qu'un seul thread puisse maintenir le verrou à la fois. Pour ce faire, créez un objet de verrouillage, puis utilisez with lock: si différents threads exécutant cette section chacun peuvent être mis en attente avant d'entrer dans ce contexte.

Le __enter__ et __exit__ les méthodes forment le protocole du gestionnaire de contexte , et vous ne devez les utiliser que si vous souhaitez réellement gérer un contexte. Le but des gestionnaires de contexte est de simplifier les try...finally et try...except modèles, pour ne pas gérer la durée de vie d'une seule instance. Voir PEP 343 - L'instruction "with" :

Ce PEP ajoute une nouvelle instruction "with" au langage Python pour permettre de factoriser les utilisations standard des instructions try/finally.

17
Martijn Pieters

del x N'appelle pas directement x.__del__()

Vous n'avez aucun contrôle sur quand .__del__ Est appelé, ou en fait s'il est appelé du tout .

Par conséquent, l'utilisation de __init__/__del__ Pour la gestion de contexte n'est pas fiable.

5
wim

En les utilisant au lieu de __init__/__del__ Il semble que je crée un contrat implicite avec les appelants qu'ils doivent utiliser with, mais il n'y a aucun moyen d'appliquer un tel contrat

Vous avez un contrat dans les deux cas. Si les utilisateurs utilisent votre objet sans se rendre compte qu'il nécessite un nettoyage après utilisation, ils bousilleront les choses, quelle que soit la façon dont vous implémentez le nettoyage. Ils peuvent conserver une référence à votre objet pour toujours, par exemple, en empêchant __del__ de courir.

Si vous avez un objet qui nécessite un nettoyage spécial, vous devez rendre cette exigence explicite. Vous devez donner aux utilisateurs la fonctionnalité with et une méthode explicite close ou une méthode similaire, pour permettre aux utilisateurs de contrôler le moment du nettoyage. Vous ne pouvez pas masquer l'exigence de nettoyage dans un __del__ méthode. Vous souhaiterez peut-être implémenter __del__ aussi, par mesure de sécurité, mais vous ne pouvez pas utiliser __del__à la place dewith ou un close explicite.


Cela dit, Python ne fait aucune promesse que __del__ s'exécutera, jamais. L'implémentation standard s'exécutera __del__ lorsque le décompte d'un objet tombe à 0, mais cela peut ne pas se produire si une référence survit à la fin du script, ou si l'objet est dans un cycle de référence. Les autres implémentations n'utilisent pas le refcounting, ce qui rend __del__ encore moins prévisible.