Explicando '__enter__' y '__exit__' de Python

364

Vi esto en el código de alguien. Qué significa eso?

    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
fuente
19
Una buena explicación aquí: effbot.org/zone/python-with-statement.htm
Manur
77
@StevenVascellaro Editar el código de una pregunta generalmente es una mala idea, especialmente cuando hay errores en el código. Esta pregunta se hizo con Py2 en mente, y no hay razón para actualizarla a Py3.
jpaugh

Respuestas:

421

El uso de estos métodos mágicos ( __enter__, __exit__) le permite implementar objetos que se pueden usar fácilmente con la withinstrucción.

La idea es que facilita la creación de código que necesita algún código 'cleandown' ejecutado (piense en él como un try-finallybloque). Alguna explicación más aquí .

Un ejemplo útil podría ser un objeto de conexión de base de datos (que luego cierra automáticamente la conexión una vez que la declaración 'with' correspondiente queda fuera de alcance):

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()
        ...

Como se explicó anteriormente, use este objeto con la withdeclaración (es posible que deba hacerlo from __future__ import with_statementen la parte superior del archivo si está en Python 2.5).

with DatabaseConnection() as mydbconn:
    # do stuff

PEP343 - La declaración ' con ' también tiene una buena redacción.

ChristopheD
fuente
20
Probablemente, __enter__debería volver selfsiempre, ya que solo se pueden invocar otros métodos de la clase en el contexto.
ViFI
3
@ViFI Hay 4 ejemplos de def __enter__(self)en PEP 343 y nadie lo hace return self: python.org/dev/peps/pep-0343 . ¿Por qué piensas eso?
Dolor
44
@Grief: por 2 razones, en mi opinión, 1) no podré llamar a otros métodos en el selfobjeto como se explica aquí: stackoverflow.com/questions/38281853/… 2) self.XYZ es solo parte del objeto propio y devolver el asa solo a eso me parece inapropiado desde el punto de vista del mantenimiento. Preferiría devolver el identificador para completar el objeto y luego proporcionar API públicas solo a los selfobjetos de componentes , que quiero exponer al usuario como en with open(abc.txt, 'r') as fin: content = fin.read()
ViFI
44
Objetos de archivo vuelven selfa partir __enter__, que es ¿cómo es que puede procesar el archivo como fen el interiorwith open(...) as f
holdenweb
2
Una sutileza que tenía que entender: si el objeto requiere parámetros para inicializarse, esos deberían estar en init , no en uno mismo .
dfrankow
70

Si sabes qué son los gestores de contexto, entonces no necesitas nada más que entender __enter__y __exit__métodos mágicos. Veamos un ejemplo muy simple.

En este ejemplo, estoy abriendo myfile.txt con la ayuda de la función de apertura . El bloque try / finally asegura que incluso si se produce una excepción inesperada, myfile.txt se cerrará.

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

Ahora estoy abriendo el mismo archivo con la declaración:

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

Si observa el código, no cerré el archivo y no hay ningún intento / finalmente bloqueo. Porque con la declaración se cierra automáticamente myfile.txt . Incluso puede verificarlo llamando al print(fp.closed)atributo, que regresa True.

Esto se debe a que los objetos de archivo (fp en mi ejemplo) devueltos por la función abierta tienen dos métodos integrados __enter__y __exit__. También se conoce como gestor de contexto. __enter__El método se llama al comienzo de with block y el __exit__ método se llama al final. Nota: con la declaración sólo funciona con objetos que soportan el protocolo contexto mamangement es decir, tienen __enter__y __exit__métodos. Una clase que implementa ambos métodos se conoce como clase de administrador de contexto.

Ahora definamos nuestra propia clase de gestor de contexto .

 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")

Espero que ahora tengas una comprensión básica de ambos __enter__y de __exit__los métodos mágicos.

N Randhawa
fuente
53

Google me pareció extrañamente difícil localizar los documentos __enter__y __exit__métodos de Python, así que para ayudar a otros aquí está el enlace:

https://docs.python.org/2/reference/datamodel.html#with-statement-context-managers
https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers
(el detalle es el mismo para ambas versiones)

object.__enter__(self)
Ingrese el contexto de tiempo de ejecución relacionado con este objeto. La withdeclaración vinculará el valor de retorno de este método a los objetivos especificados en la cláusula as de la declaración, si corresponde.

object.__exit__(self, exc_type, exc_value, traceback)
Salga del contexto de tiempo de ejecución relacionado con este objeto. Los parámetros describen la excepción que provocó la salida del contexto. Si se salió del contexto sin una excepción, los tres argumentos lo serán None.

Si se proporciona una excepción, y el método desea suprimir la excepción (es decir, evitar que se propague), debería devolver un valor verdadero. De lo contrario, la excepción se procesará normalmente al salir de este método.

Tenga en cuenta que los __exit__()métodos no deben volver a plantear la excepción pasada; Esta es la responsabilidad de la persona que llama.

Esperaba una descripción clara de los __exit__argumentos del método. Esto falta pero podemos deducirlos ...

Presumiblemente exc_typees la clase de la excepción.

Dice que no debe volver a plantear la excepción aprobada. Esto nos sugiere que uno de los argumentos podría ser una instancia de excepción real ... ¿o tal vez se supone que debes instanciarlo tú mismo a partir del tipo y el valor?

Podemos responder mirando este artículo:
http://effbot.org/zone/python-with-statement.htm

Por ejemplo, el siguiente __exit__método se traga cualquier TypeError, pero deja pasar todas las demás excepciones:

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

... tan claramente valuees una instancia de excepción.

Y presumiblemente tracebackes un objeto de rastreo de Python .

Anéntrópico
fuente
2
De acuerdo. Esta url es muy difícil de encontrar.
Shihao Xu
podría ser importante tener en cuenta este bit oculto
Tcll
43

Además de las respuestas anteriores para ejemplificar el orden de invocación, un ejemplo de ejecución 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")

Produce la salida:

__init__
__enter__
body
__exit__
__del__

Un recordatorio: cuando se usa la sintaxis with myclass() as mc, la variable mc obtiene el valor devuelto por __enter__(), en el caso anterior None. Para tal uso, necesita definir el valor de retorno, como:

def __enter__(self): 
    print('__enter__')
    return self
Yuri Feldman
fuente
3
E incluso si se cambia la secuencia de las definiciones, ¡el orden de ejecución sigue siendo el mismo!
Sean
1
Esto fue muy útil. Gracias.
Reez0
5

Intente agregar mis respuestas (mi pensamiento de aprender):

__enter__y [__exit__]ambos son métodos que se invocan al entrar y salir del cuerpo de " la declaración con " ( PEP 343 ) y la implementación de ambos se llama administrador de contexto.

la declaración with tiene la intención de ocultar el control de flujo de la cláusula try finalmente y hacer que el código sea inescrutable.

La sintaxis de la instrucción with es:

with EXPR as VAR:
    BLOCK

que se traducen a (como se menciona en 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)

prueba un código:

>>> 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'

e intente ahora manualmente (siguiendo la sintaxis de traducción):

>>> 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

el resultado del lado del servidor igual que antes

lo siento por mi mal inglés y mis explicaciones poco claras, gracias ...

Wira Bhakti
fuente
1

Esto se llama administrador de contexto y solo quiero agregar que existen enfoques similares para otros lenguajes de programación. Compararlos podría ser útil para comprender el administrador de contexto en python. Básicamente, se utiliza un administrador de contexto cuando se trata de algunos recursos (archivo, red, base de datos) que deben inicializarse y, en algún momento, eliminarse (desecharse). En Java 7 y superior tenemos una gestión automática de recursos que toma la forma de:

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

Tenga en cuenta que Session necesita implementar AutoClosableo una de sus (muchas) subinterfaces.

En C # , hemos utilizado declaraciones para administrar recursos que toman la forma de:

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

En el que Sessiondebe implementar IDisposable.

En python , la clase que usamos debería implementar __enter__y __exit__. Entonces toma la forma de:

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

Y como otros señalaron, siempre puede usar la declaración try / finally en todos los idiomas para implementar el mismo mecanismo. Esto es solo azúcar sintáctico.

Rohola Zandie
fuente