Expliquer '__enter__' et '__exit__' de Python

364

J'ai vu ça dans le code de quelqu'un. Qu'est-ce que ça veut dire?

    def __enter__(self):
        return self

    def __exit__(self, type, value, tb):
        self.stream.close()

from __future__ import with_statement#for python2.5 

class a(object):
    def __enter__(self):
        print 'sss'
        return 'sss111'
    def __exit__(self ,type, value, traceback):
        print 'ok'
        return False

with a() as s:
    print s


print s
zjm1126
la source
19
Une bonne explication ici: effbot.org/zone/python-with-statement.htm
Manur
7
@StevenVascellaro Modifier le code d'une question est généralement une mauvaise idée, surtout lorsqu'il y a des erreurs dans le code. Cette question a été posée en pensant à Py2, et il n'y a aucune raison de la mettre à jour vers Py3.
jpaugh

Réponses:

421

L'utilisation de ces méthodes magiques ( __enter__, __exit__) vous permet d'implémenter des objets qui peuvent être utilisés facilement avec l' withinstruction.

L'idée est qu'il facilite la construction de code qui nécessite un peu de code de «nettoyage» (pensez-y comme un try-finallybloc). Quelques explications supplémentaires ici .

Un exemple utile pourrait être un objet de connexion à une base de données (qui ferme ensuite automatiquement la connexion une fois que l'instruction «with» correspondante est hors de portée):

class DatabaseConnection(object):

    def __enter__(self):
        # make a database connection and return it
        ...
        return self.dbconn

    def __exit__(self, exc_type, exc_val, exc_tb):
        # make sure the dbconnection gets closed
        self.dbconn.close()
        ...

Comme expliqué ci-dessus, utilisez cet objet avec l' withinstruction (vous devrez peut-être le faire from __future__ import with_statementen haut du fichier si vous êtes sur Python 2.5).

with DatabaseConnection() as mydbconn:
    # do stuff

PEP343 - La déclaration "avec" a également une belle écriture.

ChristopheD
la source
20
Probablement, __enter__devrait selftoujours retourner comme alors seules les autres méthodes de la classe peuvent être appelées dans le contexte.
ViFI
3
@ViFI Il y a 4 exemples def __enter__(self)dans PEP 343 et personne ne le fait return self: python.org/dev/peps/pep-0343 . Pourquoi penses-tu ça?
Grief
4
@Grief: Pour 2 raisons, à mon avis, 1) je ne pourrai pas appeler d'autres méthodes sur l' selfobjet comme expliqué ici: stackoverflow.com/questions/38281853/… 2) self.XYZ est juste une partie de self object et revenir à la poignée uniquement pour cela me semble inapproprié du point de vue de la maintenance. Je préférerais plutôt retourner le descripteur de l'objet complet, puis fournir des API publiques uniquement à ces selfobjets composants , que je souhaite exposer à l'utilisateur comme dans with open(abc.txt, 'r') as fin: content = fin.read()
ViFI
4
Les objets fichiers reviennent selfde __enter__, c'est fwith open(...) as f
pourquoi
2
Une subtilité que je devais comprendre: si l'objet nécessite des paramètres pour s'initialiser, ceux-ci devraient être sur init , pas self .
dfrankow
70

Si vous savez ce que sont les gestionnaires de contexte, vous n'avez plus besoin de comprendre __enter__et de __exit__méthodes magiques. Voyons un exemple très simple.

Dans cet exemple, j'ouvre myfile.txt à l'aide de la fonction open . Le bloc try / finally garantit que même si une exception inattendue se produit, myfile.txt sera fermé.

fp=open(r"C:\Users\SharpEl\Desktop\myfile.txt")
try:
    for line in fp:
        print(line)
finally:
    fp.close()

Maintenant, j'ouvre le même fichier avec une déclaration:

with open(r"C:\Users\SharpEl\Desktop\myfile.txt") as fp:
    for line in fp:
        print(line) 

Si vous regardez le code, je n'ai pas fermé le fichier et il n'y a pas de bloc try / finally . Parce que l' instruction ferme automatiquement monfichier.txt . Vous pouvez même le vérifier en appelant l' print(fp.closed)attribut - qui renvoie True.

En effet, les objets fichier (fp dans mon exemple) renvoyés par la fonction open ont deux méthodes intégrées __enter__et __exit__. Il est également appelé gestionnaire de contexte. __enter__La méthode est appelée au début de with block et la __exit__ méthode est appelée à la fin. Remarque: avec l' instruction ne fonctionne qu'avec des objets qui prennent en charge le protocole de mamangement de contexte, c'est-à-dire qu'ils ont __enter__et des __exit__méthodes. Une classe qui implémente les deux méthodes est appelée classe de gestionnaire de contexte.

Permet maintenant de définir notre propre classe de gestionnaire de contexte .

 class Log:
    def __init__(self,filename):
        self.filename=filename
        self.fp=None    
    def logging(self,text):
        self.fp.write(text+'\n')
    def __enter__(self):
        print("__enter__")
        self.fp=open(self.filename,"a+")
        return self    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("__exit__")
        self.fp.close()

with Log(r"C:\Users\SharpEl\Desktop\myfile.txt") as logfile:
    print("Main")
    logfile.logging("Test1")
    logfile.logging("Test2")

J'espère que vous avez maintenant une compréhension de base des deux __enter__et __exit__des méthodes magiques.

N Randhawa
la source
53

J'ai trouvé étrangement difficile de localiser les documents __enter__et les __exit__méthodes de python par Google, donc pour aider les autres, voici le lien:

https://docs.python.org/2/reference/datamodel.html#with-statement-context-managers
https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers
(le détail est le même pour les deux versions)

object.__enter__(self)
Entrez le contexte d'exécution associé à cet objet. L' withinstruction liera la valeur de retour de cette méthode aux cibles spécifiées dans la clause as de l'instruction, le cas échéant.

object.__exit__(self, exc_type, exc_value, traceback)
Quittez le contexte d'exécution associé à cet objet. Les paramètres décrivent l'exception qui a provoqué la sortie du contexte. Si le contexte a été quitté sans exception, les trois arguments le seront None.

Si une exception est fournie et que la méthode souhaite supprimer l'exception (c'est-à-dire l'empêcher de se propager), elle doit renvoyer une valeur vraie. Sinon, l'exception sera traitée normalement à la sortie de cette méthode.

Notez que les __exit__()méthodes ne doivent pas relancer l'exception transmise; c'est la responsabilité de l'appelant.

J'espérais une description claire des __exit__arguments de la méthode. Cela fait défaut mais on peut les déduire ...

C'est probablement exc_typela classe de l'exception.

Il indique que vous ne devez pas relancer l'exception transmise. Cela nous suggère que l'un des arguments pourrait être une instance d'exception réelle ... ou peut-être êtes-vous censé l'instancier vous-même à partir du type et de la valeur?

Nous pouvons répondre en consultant cet article:
http://effbot.org/zone/python-with-statement.htm

Par exemple, la __exit__méthode suivante avale tout TypeError, mais laisse passer toutes les autres exceptions:

def __exit__(self, type, value, traceback):
    return isinstance(value, TypeError)

... est donc clairement valueune instance d'exception.

Et tracebackest probablement un objet traceback Python .

Anentropique
la source
2
Se mettre d'accord. Cette URL est si difficile à trouver.
Shihao Xu
pourrait être important de noter ce bit enfoui dans la référence PEP en notant l'utilisation d'argument: python.org/dev/peps/pep-0343/#generator-decorator
Tcll
43

En plus des réponses ci-dessus pour illustrer l'ordre d'invocation, un exemple d'exécution simple

class myclass:
    def __init__(self):
        print("__init__")

    def __enter__(self): 
        print("__enter__")

    def __exit__(self, type, value, traceback):
        print("__exit__")

    def __del__(self):
        print("__del__")

with myclass(): 
    print("body")

Produit la sortie:

__init__
__enter__
body
__exit__
__del__

Un rappel: lors de l'utilisation de la syntaxe with myclass() as mc, la variable mc obtient la valeur retournée par __enter__(), dans le cas ci-dessus None! Pour une telle utilisation, vous devez définir une valeur de retour, telle que:

def __enter__(self): 
    print('__enter__')
    return self
Yuri Feldman
la source
3
Et même si la séquence des définitions est inversée, l'ordre d'exécution reste le même!
Sean
1
Ce fut très utile. Je vous remercie.
Reez0
5

essayez d'ajouter mes réponses (ma pensée d'apprendre):

__enter__et les [__exit__]deux sont des méthodes qui sont appelées à l'entrée et à la sortie du corps de " l'instruction with " ( PEP 343 ) et leur mise en œuvre est appelée gestionnaire de contexte.

l'instruction with a pour but de masquer le contrôle de flux de la clause try finally et de rendre le code impénétrable.

la syntaxe de l'instruction with est:

with EXPR as VAR:
    BLOCK

qui se traduisent par (comme mentionné dans PEP 343):

mgr = (EXPR)
exit = type(mgr).__exit__  # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
    try:
        VAR = value  # Only if "as VAR" is present
        BLOCK
    except:
        # The exceptional case is handled here
        exc = False
        if not exit(mgr, *sys.exc_info()):
            raise
        # The exception is swallowed if exit() returns true
finally:
    # The normal and non-local-goto cases are handled here
    if exc:
        exit(mgr, None, None, None)

essayez du code:

>>> import logging
>>> import socket
>>> import sys

#server socket on another terminal / python interpreter
>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> s.listen(5)
>>> s.bind((socket.gethostname(), 999))
>>> while True:
>>>    (clientsocket, addr) = s.accept()
>>>    print('get connection from %r' % addr[0])
>>>    msg = clientsocket.recv(1024)
>>>    print('received %r' % msg)
>>>    clientsocket.send(b'connected')
>>>    continue

#the client side
>>> class MyConnectionManager:
>>>     def __init__(self, sock, addrs):
>>>         logging.basicConfig(level=logging.DEBUG, format='%(asctime)s \
>>>         : %(levelname)s --> %(message)s')
>>>         logging.info('Initiating My connection')
>>>         self.sock = sock
>>>         self.addrs = addrs
>>>     def __enter__(self):
>>>         try:
>>>             self.sock.connect(addrs)
>>>             logging.info('connection success')
>>>             return self.sock
>>>         except:
>>>             logging.warning('Connection refused')
>>>             raise
>>>     def __exit__(self, type, value, tb):
>>>             logging.info('CM suppress exception')
>>>             return False
>>> addrs = (socket.gethostname())
>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> with MyConnectionManager(s, addrs) as CM:
>>>     try:
>>>         CM.send(b'establishing connection')
>>>         msg = CM.recv(1024)
>>>         print(msg)
>>>     except:
>>>         raise
#will result (client side) :
2018-12-18 14:44:05,863         : INFO --> Initiating My connection
2018-12-18 14:44:05,863         : INFO --> connection success
b'connected'
2018-12-18 14:44:05,864         : INFO --> CM suppress exception

#result of server side
get connection from '127.0.0.1'
received b'establishing connection'

et essayez maintenant manuellement (suivant la syntaxe de traduction):

>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #make new socket object
>>> mgr = MyConnection(s, addrs)
2018-12-18 14:53:19,331         : INFO --> Initiating My connection
>>> ext = mgr.__exit__
>>> value = mgr.__enter__()
2018-12-18 14:55:55,491         : INFO --> connection success
>>> exc = True
>>> try:
>>>     try:
>>>         VAR = value
>>>         VAR.send(b'establishing connection')
>>>         msg = VAR.recv(1024)
>>>         print(msg)
>>>     except:
>>>         exc = False
>>>         if not ext(*sys.exc_info()):
>>>             raise
>>> finally:
>>>     if exc:
>>>         ext(None, None, None)
#the result:
b'connected'
2018-12-18 15:01:54,208         : INFO --> CM suppress exception

le résultat du côté serveur comme avant

désolé pour mon mauvais anglais et mes explications peu claires, merci ....

Wira Bhakti
la source
1

C'est ce qu'on appelle le gestionnaire de contexte et je veux juste ajouter que des approches similaires existent pour d'autres langages de programmation. Les comparer pourrait être utile pour comprendre le gestionnaire de contexte en python. Fondamentalement, un gestionnaire de contexte est utilisé lorsque nous avons affaire à certaines ressources (fichier, réseau, base de données) qui doivent être initialisées et, à un moment donné, détruites (éliminées). Dans Java 7 et supérieur, nous avons une gestion automatique des ressources qui prend la forme de:

//Java code
try (Session session = new Session())
{
  // do stuff
}

Notez que Session doit implémenter AutoClosableou l'une de ses (nombreuses) sous-interfaces.

En C # , nous utilisons des instructions pour gérer les ressources qui prennent la forme de:

//C# code
using(Session session = new Session())
{
  ... do stuff.
}

Dans lequel Sessiondevrait mettre en œuvre IDisposable.

En python , la classe que nous utilisons doit implémenter __enter__et __exit__. Il prend donc la forme de:

#Python code
with Session() as session:
    #do stuff

Et comme d'autres l'ont souligné, vous pouvez toujours utiliser l'instruction try / finally dans toutes les langues pour implémenter le même mécanisme. Ce n'est que du sucre syntaxique.

Rohola Zandie
la source