Tengo el siguiente código:
class Test: def __init__(self, name): self.name = name def __enter__(self): print(f'entering {self.name}') def __exit__(self, exctype, excinst, exctb) -> bool: print(f'exiting {self.name}') return True with Test('first') as test: print(f'in {test.name}') test = Test('second') with test: print(f'in {test.name}')Ejecutarlo produce el siguiente resultado:
entering first exiting first entering second in second exiting secondPero esperaba que produjera:
entering first in first exiting first entering second in second exiting second¿Por qué no se llama el código dentro de mi primer ejemplo?
El método __enter__ debería devolver el objeto de contexto. with ... as ... usa el valor de retorno de __enter__ para determinar qué objeto darle. Dado que su __enter__ no devuelve nada, implícitamente devuelve None , por lo que test es None .
with Test('first') as test: print(f'in {test.name}') test = Test('second') with test: print(f'in {test.name}') Así que test es ninguna. Entonces test.name es un error. Ese error se genera, por lo que se llama a Test('first').__exit__ . __exit__ devuelve True , lo que indica que el error se manejó (esencialmente, que su __exit__ está actuando como un bloque de except ), por lo que el código continúa después del primer bloque with , ya que le dijo a Python que todo estaba bien.
Considerar
def __enter__(self): print(f'entering {self.name}') return self También puede considerar no devolver True de __exit__ a menos que realmente tenga la intención de suprimir incondicionalmente todos los errores en el bloque (y comprenda completamente las consecuencias de suprimir los errores de otros programadores, así como KeyboardInterrupt , StopIteration y varias señales del sistema)
Creo que este comportamiento se debe a que __enter__ debe devolver algo sobre lo que se operará, que en este caso se accederá con el nombre test . Cambiando __enter__ a lo siguiente
def __enter__(self): print(f"entering {self.name}") return selfobtenemos el comportamiento esperado.
El problema es que su método __enter__ devuelve None . Por lo tanto, a test se le asigna None .
Luego intenta acceder a (None).name , lo que genera un error. Dado que su método __exit__ siempre devuelve True , suprimirá cualquier error. Según los documentos :
Devolver un valor verdadero de este método hará que la declaración with suprima la excepción y continúe la ejecución con la declaración que sigue inmediatamente a la declaración with.
__enter__ está dando None como salida, ya que no hay return , por lo tanto, activaría directamente __exit__ ya que None no tiene un name de atributo, por ejemplo:
>>> None.name Traceback (most recent call last): File "<pyshell#0>", line 1, in <module> None.__name__ AttributeError: 'NoneType' object has no attribute 'name' >>> Si lo configura para llamar a __class__.__name__ ( None de los objetos tiene ese atributo, lo que da NoneType ), podría encontrar el problema fácilmente:
class Test: def __init__(self, name): self.name = name def __enter__(self): print(f'entering {self.name}') def __exit__(self, exctype, excinst, exctb) -> bool: print(f'exiting {self.name}') return True with Test('first') as test: print(f'in {test.__class__.__name__}') test = Test('second') with test: print(f'in {test.__class__.__name__}')Producción:
entering first in NoneType exiting first entering second in Test exiting second Como puede ver, dice in NoneType , no devolver ningún valor es la razón de esto. En muchos casos, __enter__ no necesita regresar, pero en este caso, la clase Test necesita que regrese.
La solución sería mantener la instancia de Test , de modo que llame al name de un self devuelto después del resultado __enter__ del administrador de contexto. Hasta ahora, __enter__ da como resultado None , por lo tanto, el atributo None.name no existe. Entonces, si devuelve self , existiría el atributo test.name .
La solución sería devolverse a self en la implementación del método mágico __enter__ :
... def __enter__(self): print(f'entering {self.name}') return self ...Código completo:
class Test: def __init__(self, name): self.name = name def __enter__(self): print(f'entering {self.name}') return self def __exit__(self, exctype, excinst, exctb) -> bool: print(f'exiting {self.name}') return True with Test('first') as test: print(f'in {test.name}') test = Test('second') with test: print(f'in {test.name}')Producción:
entering first in first exiting first entering second in second exiting second La información adicional que di que las otras respuestas no dieron es una prueba más concreta de la implementación del método __enter__ que da None . También mostré un ejemplo de ello.
La razón es que el primer y segundo caso no hacen lo mismo.
en first :
__init__ ;with llamadas __enter__ ;as almacena el resultado de __enter__ en la test__enter__ no tiene un valor de retorno, test es None . en second :
__init__ ;test ;with llamadas __enter__ ;__enter__ ;test sigue refiriéndose al objeto que se creó originalmente. En ambos casos, se llama a __exit__ para el objeto with se está manejando, por lo que verá que se imprime la etiqueta correcta; es solo que, en first , el identificador de test no está vinculado a ese mismo objeto.
NB __enter__ no tiene que devolver self . Puede devolver algo completamente diferente, por ejemplo, puede abrir un archivo y hacer que __enter__ devuelva la secuencia, mientras que __exit__ puede cerrarlo. Si se diera por sentado que __enter__ debería devolver self , eso sería redundante y simplemente podría estar implícito.