Explicando '__enter__' e '__exit__' do Python

363

Eu vi isso no código de alguém. O que isso significa?

    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
fonte
19
Uma boa explicação aqui: effbot.org/zone/python-with-statement.htm
Manur
7
@StevenVascellaro Editar o código de uma pergunta geralmente é uma má ideia, especialmente quando há erros no código. Esta pergunta foi feita com o Py2 em mente e não há motivo para atualizá-lo para o Py3.
jpaugh

Respostas:

420

O uso desses métodos mágicos ( __enter__, __exit__) permite implementar objetos que podem ser usados ​​facilmente com a withinstrução

A idéia é que facilita a criação de código que precisa de algum código 'cleandown' executado (pense nele como um try-finallybloco). Mais algumas explicações aqui .

Um exemplo útil pode ser um objeto de conexão com o banco de dados (que fecha automaticamente a conexão assim que a instrução 'with' correspondente estiver fora do escopo):

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 explicado acima, use esse objeto com a withinstrução (pode ser necessário fazer from __future__ import with_statementna parte superior do arquivo se você estiver no Python 2.5).

with DatabaseConnection() as mydbconn:
    # do stuff

PEP343 - A declaração 'with' também possui uma boa redação.

ChristopheD
fonte
20
Provavelmente, __enter__deve retornar selfsempre, pois somente outros métodos da classe podem ser chamados no contexto.
ViFI
3
@ ViFI Existem 4 exemplos def __enter__(self)no PEP 343 e ninguém o faz return self: python.org/dev/peps/pep-0343 . Porque você acha isso?
Luto
4
@ Grief: Por 2 razões, na minha opinião, 1) não poderei chamar outros métodos no selfobjeto, conforme explicado aqui: stackoverflow.com/questions/38281853/… 2) self.XYZ é apenas parte do auto-objeto e retornando o identificador apenas para o que me parece inadequado do ponto de vista da manutenção. Eu preferiria preferem voltar alça para completa objeto e, em seguida, fornecer APIs públicas para apenas os componentes selfobjeto, o que eu quero expor para o usuário como em with open(abc.txt, 'r') as fin: content = fin.read()
ViFI
4
Objetos de arquivo voltar selfa partir __enter__, que é como é que você pode processar o arquivo como fdentrowith open(...) as f
holdenweb
2
Uma sutileza que eu tinha que entender: se o objeto requer parâmetros para inicializar, eles devem estar no init , não no próprio .
dfrankow
70

Se você sabe o que são gerenciadores de contexto , não precisa mais nada para entender __enter__e __exit__métodos mágicos. Vamos ver um exemplo muito simples.

Neste exemplo, estou abrindo myfile.txt com a ajuda da função open . O bloco try / finally garante que, mesmo que ocorra uma exceção inesperada, myfile.txt seja fechado.

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

Agora estou abrindo o mesmo arquivo com a declaração:

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

Se você olhar o código, não fechei o arquivo e não há bloco try / finalmente . Porque com a instrução fecha automaticamente myfile.txt . Você pode até checar chamando print(fp.closed)atributo - que retorna True.

Isso ocorre porque os objetos de arquivo (fp no meu exemplo) retornados pela função open possuem dois métodos internos __enter__e __exit__. Também é conhecido como gerenciador de contexto. __enter__O método é chamado no início de with block e o __exit__ método é chamado no final. Nota: with statement funciona apenas com objetos que suportam o protocolo de gerenciamento de contexto, ou seja, eles têm __enter__e __exit__métodos. Uma classe que implementa os dois métodos é conhecida como classe de gerenciador de contexto.

Agora vamos definir nossa própria classe de gerenciador 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 agora você tenha uma compreensão básica dos métodos __enter__e dos __exit__métodos mágicos.

N Randhawa
fonte
53

Achei estranhamente difícil localizar os documentos __enter__e __exit__métodos python no Google , para ajudar outras pessoas aqui no link:

https://docs.python.org/2/reference/datamodel.html#with-statement-context-managers
https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers
(o detalhe é o mesmo para as duas versões)

object.__enter__(self)
Digite o contexto de tempo de execução relacionado a este objeto. A withinstrução vinculará o valor de retorno desse método aos destinos especificados na cláusula as da instrução, se houver.

object.__exit__(self, exc_type, exc_value, traceback)
Saia do contexto de tempo de execução relacionado a este objeto. Os parâmetros descrevem a exceção que causou a saída do contexto. Se o contexto foi encerrado sem uma exceção, todos os três argumentos serão None.

Se uma exceção é fornecida e o método deseja suprimir a exceção (ou seja, impedir que ela seja propagada), ele deve retornar um valor verdadeiro. Caso contrário, a exceção será processada normalmente após a saída deste método.

Observe que os __exit__()métodos não devem reraise a exceção transmitida; isso é responsabilidade do chamador.

Eu esperava uma descrição clara dos __exit__argumentos do método. Isso está faltando, mas podemos deduzi-los ...

Presumivelmente, exc_typeé a classe da exceção.

Ele diz que você não deve aumentar novamente a exceção transmitida. Isso nos sugere que um dos argumentos pode ser uma instância de exceção real ... ou talvez você deva instanciar você mesmo a partir do tipo e valor?

Podemos responder consultando este artigo:
http://effbot.org/zone/python-with-statement.htm

Por exemplo, o __exit__método a seguir engole qualquer TypeError, mas permite todas as outras exceções:

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

... é tão claramente valueuma instância de exceção.

E presumivelmente tracebacké um objeto de rastreamento de Python .

Anentrópico
fonte
2
Aceita. Este URL é tão difícil de encontrar.
Shihao Xu
pode ser importante notar este bit enterrado dentro da referência PEP observando o uso arg: python.org/dev/peps/pep-0343/#generator-decorator
Tcll
43

Além das respostas acima para exemplificar a ordem de chamada, um exemplo simples de execução

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

Produz a saída:

__init__
__enter__
body
__exit__
__del__

Um lembrete: ao usar a sintaxe with myclass() as mc, a variável mc obtém o valor retornado por __enter__(), no caso acima None! Para esse uso, é necessário definir o valor de retorno, como:

def __enter__(self): 
    print('__enter__')
    return self
Yuri Feldman
fonte
3
E mesmo que a sequência das definições seja alterada, a ordem de execução permanece a mesma!
5285 Sean
11
Isso foi muito útil. Obrigado.
Reez0 04/04
5

tente adicionar minhas respostas (meu pensamento de aprender):

__enter__e [__exit__]ambos são métodos que são chamados na entrada e saída do corpo da " declaração with " ( PEP 343 ) e a implementação de ambos é chamada de gerenciador de contexto.

a instrução with pretende ocultar o controle de fluxo da cláusula try finally e tornar o código inescrutável.

a sintaxe da instrução with é:

with EXPR as VAR:
    BLOCK

traduzidos para (como mencionado no 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)

tente algum 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 agora tente manualmente (a seguir, sintaxe de conversão):

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

o resultado do lado do servidor é o mesmo de antes

desculpe pelo meu inglês ruim e minhas explicações pouco claras, obrigado ....

Wira Bhakti
fonte
1

Isso é chamado de gerenciador de contexto e eu só quero acrescentar que abordagens semelhantes existem para outras linguagens de programação. Compará-los pode ser útil para entender o gerenciador de contexto em python. Basicamente, um gerenciador de contexto é usado quando estamos lidando com alguns recursos (arquivo, rede, banco de dados) que precisam ser inicializados e, em algum momento, derrubados (descartados). No Java 7 e acima, temos o gerenciamento automático de recursos que assume a forma de:

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

Observe que a sessão precisa implementar AutoClosableou uma de suas (muitas) subinterfaces.

Em C # , usamos instruções para gerenciar recursos que assumem a forma de:

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

Em que Sessiondeve implementar IDisposable.

Em python , a classe que usamos deve implementar __enter__e __exit__. Portanto, assume a forma de:

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

E, como outros apontaram, você sempre pode usar a instrução try / finally em todos os idiomas para implementar o mesmo mecanismo. Isso é apenas açúcar sintático.

Rohola Zandie
fonte